Язык Си

         

Операторы и выражения присваивания


Выражение

i = i + 2;

в котором стоящая слева переменная повторяется и справа, можно написать в сжатом виде:

i += 2;

Оператор +=, как и =, называется оператором присваивания.

Большинству бинарных операторов (аналогичных + и имеющих левый и правый операнды) соответствуют операторы присваивания op=, где op - один из операторов

+ - * / % >

& ^ |

Если выр1 и выр2 - выражения, то

выр1 op= выр2

Эквивалентно

выр1 = (выр1) op (выр2)

с той лишь разницей, что выр1 вычисляется только один раз. Обратите внимание на скобки вокруг выр2:

x *= y + 1

эквивалентно

x = x * (y + 1)

но не

x=x*y+1

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

/* bitcount: подсчет единиц в х */ int bitcount(unsigned х) { int b; for (b = 0; х != 0; x >>= 1) if (x & 01) b++; return b; }

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

Помимо краткости операторы присваивания обладают тем преимуществом, что они более соответствуют тому, как человек мыслит. Мы говорим "прибавить 2 к i" или "увеличить i на 2", а не "взять i, добавить 2 и затем вернуть результат в i", так что выражение i+=2 лучше, чем i=i+2. Кроме того, в сложных выражениях вроде

yyval[yypv[p3+p4] + yypv[p1+p2]]+= 2

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

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

while ((с = getchar()) != EOF)



В выражениях встречаются и другие операторы присваивания (+=, -= и т. д.), хотя и реже. Типом и значением любого выражения присваивания являются тип и значение его левого операнда после завершения присваивания.

Упражнение 2.9. Применительно к числам, в представлении которых использован дополнительный код, выражение x &= (x-1) уничтожает самую правую 1 в x. Объясните, почему. Используйте это наблюдение при написании более быстрого варианта функции bitcount.



Операторы инкремента и декремента


В Си есть два необычных оператора, предназначенных для увеличения и уменьшения переменных. Оператор инкремента ++ добавляет 1 к своему операнду, а оператор декремента -- вычитает 1. Мы уже неоднократно использовали ++ для наращивания значения переменных, как, например, в

if (c == '\n') ++nl;

Необычность операторов ++ и -- в том, что их можно использовать и как префиксные (помещая перед переменной: ++n), и как постфиксные (помещая после переменной: n++) операторы. В обоих случаях значение n увеличивается на 1, но выражение ++n увеличивает n до того, как его значение будет использовано, а n++ - после того. Предположим, что n содержит 5, тогда

x = n++;

установит x в значение 5, а

x = ++n;

установит x в значение 6. И в том и другом случае n станет равным 6. Операторы инкремента и декремента можно применять только к переменным. Выражения вроде (i+j)++ недопустимы.

Если требуется только увеличить или уменьшить значение переменной (но не получить ее значение), как например

if (c=='\n') nl++;

то безразлично, какой оператор выбрать - префиксный или постфиксный. Но существуют ситуации, когда требуется оператор вполне определенного типа. Например, рассмотрим функцию squeeze(s, c), которая удаляет из строки s все символы, совпадающие с c:

/* squeeze: удаляет все c из s*/ void squeeze(char s[], int с) { int i, j; for (i = j =0; s[i] != '\0'; i++) if (s[i] != c) s[j++] = s[i]; s[i] = '\0'; }

Каждый раз, когда встречается символ, отличный от c, он копируется в текущую j-ю позицию, и только после этого переменная j увеличивается на 1, подготавливаясь таким образом к приему следующего символа. Это в точности совпадает со следующими действиями:

if (s[i] != с) { s[j] = s[i]; j++; }

Другой пример - функция getline, которая нам известна по . Приведенную там запись

if (c =='\n') { s[i] = c; ++i; }

можно переписать более компактно:

if (с == '\n') s[i++] = с;

В качестве третьего примера рассмотрим стандартную функцию strcat(s,t), которая строку t помещает в конец строки s. Предполагается, что в s достаточно места, чтобы разместить там суммарную строку. Мы написали strcat так, что она не возвращает никакого результата. На самом деле библиотечная strcat возвращает указатель на результирующую строку.

/* strcat: помещает t в конец s; s достаточно велика */ void strcat (char s[], char t[]) { int i, j; i = j = 0; while (s[i] != '\0') /* находим конец s */ i++; while ((s[i++] = t[j++]) != '\0') /* копируем t */ ; }

При копировании очередного символа из t в s постфиксный оператор ++ применяется и к i, и к j, чтобы на каждом шаге цикла переменные i и j правильно отслеживали позиции перемещаемого символа.

Упражнение 2.4. Напишите версию функции squeeze(s1,s2), которая удаляет из s1 все символы, встречающиеся в строке s2.

Упражнение 2.5. Напишите функцию any(s1,s2), которая возвращает либо ту позицию в s1, где стоит первый символ, совпавший с любым из символов в s2, либо -1 (если ни один символ из s1 не совпадает с символами из s2). (Стандартная библиотечная функция strpbrk делает то же самое, но выдает не номер позиции символа, а указатель на символ.)



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


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

>

>= <

Все они имеют одинаковый приоритет. Сразу за ними идет приоритет операторов сравнения на равенство:

== !=

Операторы отношения имеют более низкий приоритет, чем арифметические, поэтому выражение вроде i < lim-1 будет выполняться так же, как i < (lim-1), т.е. как мы и ожидаем.

Более интересны логические операторы && и . Выражения, между которыми стоят операторы && или , вычисляются слева направо. Вычисление прекращается, как только становится известна истинность или ложность результата. Многие Си- программы опираются на это свойство, как, например, цикл из функции getline, которую мы приводили в :

for (i = 0; i < lim-1 && (с = getchar()) != EOF && с != '\n'; ++i) s[i] = c;

Прежде чем читать очередной символ, нужно проверить, есть ли для него место в массиве s, иначе говоря, сначала необходимо проверить соблюдение условия i < lim-1. Если это условие не выполняется, мы не должны продолжать вычисление, в частности читать следующий символ. Так же было бы неправильным сравнивать c и EOF до обращения к getchar; следовательно, и вызов getchar, и присваивание должны выполняться перед указанной проверкой.

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

i < lim-1 && (с = getchar()) != '\n' && с != EOF

не нуждается в дополнительных скобках. Но, так как приоритет != выше, чем приоритет присваивания, в

(с = getchar()) != '\n'

скобки необходимы, чтобы сначала выполнить присваивание, а затем сравнение с '\n'.

По определению численным результатом вычисления выражения отношения или логического выражения является 1, если оно истинно, и 0, если оно ложно.

Унарный оператор ! преобразует ненулевой операнд в 0, а нуль в 1. Обычно оператор ! используют в конструкциях вида

if (!valid)

что эквивалентно

if (valid == 0)

Трудно сказать, какая из форм записи лучше. Конструкция вида !valid хорошо читается ("если не правильно”), но в более сложных выражениях может оказаться, что ее не так-то легко понять.

Упражнение 2.2. Напишите цикл, эквивалентный приведенному выше or- циклу, не пользуясь операторами && и .



Основные сведения о функциях


Начнем с того, что сконструируем программу, печатающую те строки вводимого текста, в которых содержится некоторый "образец", заданный в виде строки символов. (Эта программа представляет собой частный случай функции grep системы UNIX.) Рассмотрим пример: в результате поиска образца "ould" в строках текста

Ah Love! could you and I with Fate conspire To grasp this sorry Scheme of Things entire, Would not we shatter it to bits -- and then Re-mould it nearer to The Heart's Desire!

мы получим

Ah Love! could you and I with Fate conspire Would not we shatter it to bits — and then Re-mould it nearer to the Heart's Desire!

Работа по поиску образца четко распадается на три этапа:

while (существует еще строка) if (строка содержит образец) напечатать ее

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

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

Чтобы решить эту задачу, мы напишем функцию strindex(s,t), которая указывает место (индекс) в строке s, где начинается строка t, или -1, если s не содержит t. Так как в Си нумерация элементов в массивах начинается с нуля, отрицательное число -1 подходит в качестве признака неудачного поиска. Если далее нам потребуется более сложное отождествление по образцу, мы просто заменим strindex на другую функцию, оставив при этом остальную часть программы без изменений. (Библиотечная функция strstr аналогична функции strindex и отличается от последней только тем, что возвращает не индекс, а указатель.)


После такого проектирования программы ее "деталировка" оказывается очевидной. Мы имеем представление о программе в целом и знаем, как взаимодействуют ее части. В нашей программе образец для поиска задается строкой-литералом, что снижает ее универсальность. В мы еще вернемся к проблеме инициализации символьных массивов и покажем, как образец сделать параметром, устанавливаемым при запуске программы. Здесь приведена несколько измененная версия функции getline, и было бы поучительно сравнить ее с версией, рассмотренной в .

#include <stdio.h> #define MAXLINE 1000 /* максимальный размер вводимой строки */

int getline(char line[], int max); int strindex(char source[], char searchfor[]);

char pattern[] ="ould"; /* образец для поиска */

/* найти все строки, содержащие образец */

main() { char line[MAXLINE]; int found = 0; while (getline(line, MAXLINE) > 0) if (strindex(line, pattern) >= 0) { printf ("%s", line); found++; } return found; }

/* getline: читает строку в s, возвращает длину */ int getline(char s[], int lim) { int c, i; i = 0; while (--lim > 0 && (c=getchar()) != EOF && с != '\n') /* I.B.: misprint was here -lim instead of --lim */ s[i++] = c; if (c == '\n') s[i++] = c; s[i] = '\0'; return i; }

/* strindex: вычисляет место t в s или выдает -1, если t нет в s */ int strindex (char s[], char t[]) { int i, j, k; for (i = 0; s[i] != '\0'; i++) { for (j = i, k = 0; t[k] != '\0' && s[j] == t[k]; j++, k++) ; if (k > 0 && t[k] == '\0') return i; } return –1; }

Определение любой функции имеет следующий вид:

тип-результата имя-функции (объявления аргументов) { объявления и инструкции

}

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

dummy() { }

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



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

Инструкция return реализует механизм возврата результата от вызываемой функции к вызывающей. За словом return может следовать любое выражение:

return выражение;

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

Вызывающая функция вправе проигнорировать возвращаемое значение. Более того, выражение в return может отсутствовать, и тогда вообще никакое значение не будет возвращено в вызывающую функцию. Управление возвращается в вызывающую функцию без результирующего значения также и в том случае, когда вычисления достигли "конца" (т. е. последней закрывающей фигурной скобки функции). Не запрещена (но должна вызывать настороженность) ситуация, когда в одной и той же функции одни return имеют при себе выражения, а другие - не имеют. Во всех случаях, когда функция "забыла" передать результат в return, она обязательно выдаст "мусор".

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

Механизмы компиляции и загрузки Си-программ, расположенных в нескольких исходных файлах, в разных системах могут различаться. В системе UNIX, например, эти работы выполняет упомянутая в команда cc. Предположим, что три функции нашего последнего примера расположены в трех разных файлах: main.с, getline.c и strindex.c. Тогда команда

cc main.с getline.c strindex.c

скомпилирует указанные файлы, поместив результат компиляции в файлы объектных модулей main.o, getline.o и strindex.o, и затем загрузит их в исполняемый файл a.out. Если обнаружилась ошибка, например в файле main.с, то его можно скомпилировать снова и результат загрузить ранее полученными объектными файлами, выполнив следующую команду:

cc main.с getline.o strindex.o

Команда cc использует стандартные расширения файлов ".с" и ".о", чтобы отличать исходные файлы от объектных.

Упражнение 4.1. Напишите функцию strindex(s, t), которая выдает позицию самого правого вхождения t в s или -1, если вхождения не обнаружено.


Основные сведения о структурах


Сконструируем несколько графических структур. В качестве основного объекта выступает точка с координатами x и y целого типа.

Указанные две компоненты можно поместить в структуру, объявленную, например, следующим образом:

struct point { int x; int y; };

Объявление структуры начинается с ключевого слова struct и содержит список объявлений, заключенный в фигурные скобки. За словом struct может следовать имя, называемое тегом структуры (от английского слова tag — ярлык, этикетка. — Примеч. пер.), point в нашем случае. Тег дает название структуре данного вида и далее может служить кратким обозначением той части объявления, которая заключена в фигурные скобки.

Перечисленные в структуре переменные называются элементами (members - В некоторых изданиях, в том числе во 2-м издании на русским языке этой книги structure members переводится как члены структуры. - Примеч. ред). Имена элементов и тегов без каких-либо коллизий могут совпадать с именами обычных переменных (т. е. не элементов), так как они всегда различимы по контексту. Более того, одни и те же имена элементов могут встречаться в разных структурах, хотя, если следовать хорошему стилю программирования, лучше одинаковые имена давать только близким по смыслу объектам.

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

struct {...} x, y, z;

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

int х, у, z;

в том смысле, что и то и другое объявляет x, y и z переменными указанного типа; и то и другое приведет к выделению памяти соответствующего размера.

Объявление структуры, не содержащей списка переменных, не резервирует памяти; оно просто описывает шаблон, или образец структуры. Однако если структура имеет тег, то этим тегом далее можно пользоваться при определении структурных объектов. Например, с помощью заданного выше описания структуры point строка


struct point pt;

определяет структурную переменную pt типа struct point. Структурную переменную при ее определении можно инициализировать, формируя список инициализаторов ее элементов в виде константных выражений:

struct point maxpt = {320, 200};

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

Доступ к отдельному элементу структуры осуществляется посредством конструкции вида:

имя-структуры.элемент

Оператор доступа к элементу структуры . соединяет имя структуры и имя элемента. Чтобы напечатать, например, координаты точки pt, годится следующее обращение к printf:

printf("%d, %d", pt.x, pt.y);

Другой пример: чтобы вычислить расстояние от начала координат (0,0) до pt, можно написать

double dist, sqrt(double);

dist = sqrt((double)pt.x * pt.x + (double)pt.y * pt.y);

Структуры могут быть вложены друг в друга. Одно из возможных представлений прямоугольника - это пара точек на углах одной из его диагоналей:



struct rect { struct point pt1; struct point pt2; };

Структура rect содержит две структуры point. Если мы объявим screen как

struct rect screen;

то

screen.pt1.x

обращается к координате x точки pt1 из screen.


Переключатель switch


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

switch (выражение) { case конст-выр: инструкции

case конст-выр: инструкции

default: инструкции

}

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

В мы написали программу, подсчитывающую число вхождений в текст каждой цифры, символов-разделителей (пробелов, табуляций и новых строк) и всех остальных символов. В ней мы использовали последовательность if...else if...else. Теперь приведем вариант этой программы с переключателем switch:

#include <stdio.h> main() /* подсчет цифр, символов-разделителей и прочих символов */ { int c, i, nwhite, nother, ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; i++) ndigit[i] = 0; while ((с = getchar()) != EOF) { switch (c) { case '0' : case '1' : case '2' : case '3' : case '4' : case '5' : case '6' : case '7' : case '8' : case '9' : ndigit[c - '0']++; break; case ' ': case '\n': case '\t': nwhite++; break; default: nother++; break; } } printf ("цифр ="); for (i = 0; i < 10; i++) printf (" %d", ndigit[i]); printf(", символов-разделителей = %d, прочих = %d\n", nwhite, nother); return 0; }

Инструкция break вызывает немедленный выход из переключателя switch. Поскольку выбор ветви case реализуется как переход на метку, то после выполнения одной ветви case, если ничего не предпринять, программа провалится вниз на следующую ветвь. Инструкции break и return — наиболее распространенные средства выхода из переключателя. Инструкция break используется также для принудительного выхода из циклов while, for и do-while (мы еще поговорим об этом чуть позже).


"Сквозное" выполнение ветвей case вызывает смешанные чувства. С одной стороны, это хорошо, поскольку позволяет несколько ветвей case объединить в одну, как мы и поступили с цифрами в нашем примере. Но с другой - это означает, что в конце почти каждой ветви придется ставить break, чтобы избежать перехода к следующей. Последовательный проход по ветвям - вещь ненадежная, это чревато ошибками, особенно при изменении программы. За исключением случая с несколькими метками для одного вычисления, старайтесь по возможности реже пользоваться сквозным проходом, но если уж вы его применяете, обязательно комментируйте эти особые места.

Добрый вам совет: даже в конце последней ветви (после default в нашем примере) помещайте инструкцию break, хотя с точки зрения логики в ней нет никакой необходимости. Но эта маленькая предосторожность спасет вас, когда однажды вам потребуется добавить в конец еще одну ветвь case.

Упражнение 3.2. Напишите функцию escape (s,t), которая при копировании текста из t в s преобразует такие символы, как новая строка и табуляция в "видимые последовательности символов" (вроде \n и \t). Используйте инструкцию switch. Напишите функцию, выполняющую обратное преобразование эскейп- последовательностей в настоящие символы.


Переменные и арифметические выражения


Приведенная ниже программа выполняет вычисления по формуле °С = (5/9)(°F-32) и печатает таблицу соответствия температур по Фаренгейту температурам по Цельсию:

0 –17 20 –6 40 4 60 15 80 26 100 37 120 48 140 60 160 71 180 82 200 93 220 104 240 115 260 126 280 137 300 148

Как и предыдущая, эта программа состоит из определения одной-единственной функции main. Она длиннее программы, печатающей “здравствуй, мир”, но по сути не сложнее. На ней мы продемонстрируем несколько новых возможностей, включая комментарий, объявления, переменные, арифметические выражения, циклы и форматный вывод.

#include <stdio.h> /* печать таблицы температур по Фаренгейту и Цельсию для fahr = 0, 20, ..., 300 */

main() {     int fahr, celsius;     int lower, upper, step;

lower = 0; /* нижняя граница таблицы температур */     upper = 300; /* верхняя граница */ step = 20; /* шаг */

    fahr = lower;     while (fahr <= upper) {         celsius = 5 * (fahr-32) / 9;         printf(“%d\t%d\n”, fahr, celsius);         fahr = fahr + step; } }

Две строки:

/* печать таблицы температур по Фаренгейту и Цельсию для fahr = 0, 20, ... 300 */

являются комментарием, который в данном случае кратко объясняет, что делает программа. Все символы, помещенные между /* и */, игнорируются компилятором, и этим можно свободно пользоваться, чтобы сделать программу более понятной. Комментарий можно располагать в любом месте, где могут стоять символы пробела, табуляции или символ новой строки.

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

описываются свойства переменных. Оно состоит из названия типа и списка переменных, например:

int fahr, celsius; int lower, upper, step;

Тип int означает, что значения перечисленных переменных есть целые, в отличие от него тип float

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


Числа типа int бывают как 16-разрядные (лежат в диапазоне от -32768 до 32767), так и 32-разрядные. Числа типа float обычно представляются 32-разрядными словами, имеющими по крайней мере 6 десятичных значащих цифр (лежат приблизительно в диапазоне от 10-38 до 10+38.

Помимо int и float в Си имеется еще несколько базовых типов для данных, это:

char - символ-единичный байт;

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

long - длинное целое;

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

Размеры объектов указанных типов также зависят от машины. Из базовых типов можно создавать: массивы, структуры и объединения, указатели на объекты базовых типов и функции, возвращающие значения этих типов в качестве результата. Обо всем этом мы расскажем позже.

Вычисления в программе преобразования температур начинаются с инструкций присваивания.

lower = 0; upper = 300; step = 20; fahr = lower;

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

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

while(fahr <= upper) { ... }

Он работает следующим образом. Проверяется условие в скобках. Если оно истинно (значение fahr меньше или равно значению upper

), то выполняется тело цикла (три инструкции, заключенные в фигурные скобки). Затем опять проверяется условие, и если оно истинно, то тело цикла выполняется снова. Когда условие становится ложным (fahr превысило upper), цикл завершается, и вычисления продолжаются с инструкции, следующей за циклом. Поскольку никаких инструкций за циклом нет, программа завершает работу.

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

(while i < j) i = 2 * i;

И в том и в другом случае инструкции, находящиеся под управлением while, мы будем записывать со сдвигом, равным одной позиции табуляции, которая в программе указывается четырьмя пробелами; благодаря этому будут ясно видны инструкции, расположенные внутри цикла. Отступы подчеркивают логическую структуру программы. Си-компилятор не обращает внимания на внешнее оформление программы, но наличие в нужных местах отступов и пробелов существенно влияет на то, насколько легко она будет восприниматься человеком при просмотре. Чтобы лучше была видна логическая структура выражения, мы рекомендуем на каждой строке писать только по одной инструкции и с обеих сторон от операторов ставить пробелы. Положение скобок не так важно, хотя существуют различные точки зрения на этот счет. Мы остановились на одном из нескольких распространенных стилей их применения. Выберите тот, который больше всего вам нравится, и строго ему следуйте.



Большая часть вычислений выполняется в теле цикла. Температура по Фаренгейту переводится в температуру по Цельсию и присваивается переменной celsius посредством инструкции

celsius = 5 * (fahr-32) / 9;

Причина, по которой мы сначала умножаем на 5 и затем делим на 9, а не сразу умножаем на 5/9, связана с тем, что в Си, как и во многих других языках, деление целых сопровождается отбрасыванием, т. е. потерей дробной части. Так как 5 и 9 - целые, отбрасывание в 5/9 дало бы нуль, и на месте температур по Цельсию были бы напечатаны нули.

Этот пример прибавил нам еще немного знаний о том, как работает функция printf. Функция printf - это универсальная функция форматного ввода-вывода, которая будет подробно описана в . Ее первый аргумент - строка символов, в которой каждый символ % соответствует одному из последующих аргументов (второму, третьему, ...), а информация, расположенная за символом %, указывает на вид, в котором выводится каждый из этих аргументов. Например, %d специфицирует выдачу аргумента в виде целого десятичного числа, и инструкция

printf(“%d\t%d\n”, fahr, celsius);

печатает целое fahr, выполняет табуляцию (\t) и печатает целое celsius.

В функции printf каждому спецификатору первого аргумента (конструкции, начинающейся с %) соответствует второй аргумент, третий аргумент и т. д. Спецификаторы и соответствующие им аргументы должны быть согласованы по количеству и типам: в противном случае напечатано будет не то, что нужно.

Кстати, printf не является частью языка Си, и вообще в языке нет никаких специальных конструкций, определяющих ввод-вывод. Функция printf - лишь полезная функция стандартной библиотеки, которая обычно доступна для Си-программ. Поведение функции printf, однако, оговорено стандартом ANSI, и ее свойства должны быть одинаковыми во всех Си-системах, удовлетворяющих требованиям стандарта.

Желая сконцентрировать ваше внимание на самом Си, мы не будем много говорить о вводе-выводе до . В частности, мы отложим разговор о форматном вводе. Если вам потребуется ввести числа, советуем прочитать в то, что касается функции scanf. Эта функция отличается от printf лишь тем, что она вводит данные, а не выводит.



Существуют еще две проблемы, связанные с программой преобразования температур. Одна из них (более простая) состоит в том, что выводимый результат выглядит несколько неряшливо, поскольку числа не выровнены по правой позиции колонок. Это легко исправить, добавив в каждый из спецификаторов формата %d указание о ширине поля; при этом программа будет печатать числа, прижимая их к правому краю указанных полей. Например, мы можем написать

printf(“%3d%6d\n”, fahr, celsius);

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

0 -17 20 -6 40 4 60 15 80 26 100 37

Вторая, более серьезная проблема связана с тем, что мы пользуемся целочисленной арифметикой и поэтому не совсем точно вычисляем температуры по шкале Цельсия. Например, 0°F на самом деле (с точностью до десятой) равно -17.8°С, а не -17. Чтобы получить более точные значения температур, нам надо пользоваться не целочисленной арифметикой, а арифметикой с плавающей точкой. Это потребует некоторых изменений в программе.

#include <stdio.h> /* печать таблицы температур по Фаренгейту и Цельсию для fahr = 0, 20, . .., 300; вариант с плавающей точкой */ main() { float fahr, celsius; int lower, upper, step;

lower = 0; /* нижняя граница таблицы температур */ upper = 300; /* верхняя граница */ step = 20; /* шаг */

fahr = lower; while (fahr

Программа мало изменилась. Она отличается от предыдущей лишь тем, что fahr и celsius объявлены как float, а формула преобразования написана в более естественном виде. В предыдущем варианте нельзя было писать 5/9, так как целочисленное деление в результате обрезания дало бы нуль. Десятичная точка в константе указывает на то, что последняя рассматривается как число с плавающей точкой, и 5.0/9.0, таким образом, есть частное от деления двух значений с плавающей точкой, которое не предполагает отбрасывания дробной части. В том случае, когда арифметическая операция имеет целые операнды, она выполняется по правилам целочисленной арифметики. Если же один операнд с плавающей точкой, а другой - целый, то перед тем, как операция будет выполнена, последний будет преобразован в число с плавающей точкой. Если бы мы написали fahr-32 то 32 автоматически было бы преобразовано в число с плавающей точкой. Тем не менее при записи констант с плавающей точкой мы всегда используем десятичную точку, причем даже в тех случаях, когда константы на самом деле имеют целые значения. Это делается для того, чтобы обратить внимание читающего программу на их природу.



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

fahr=lower;

и проверка

while(fahr <= upper)

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

Спецификация %3.0f в printf определяет печать числа с плавающей точкой (в данном случае числа fahr) в поле шириной не более трех позиций без десятичной точки и дробной части. Спецификация %6.1f описывает печать другого числа (celsius) в поле из шести позиций с одной цифрой после десятичной точки. Напечатано будет следующее:

0 -17.8 20 -6.7 40 4.4

Ширину и точность можно не задавать; %6f означает, что число будет занимать не более шести позиций; %.2f - число имеет две цифры после десятичной точки, но ширина не ограничена; %f просто указывает на печать числа с плавающей точкой.

%d - печать десятичного целого.
%6d - печать десятичного целого в поле из шести позиций.
%f - печать числа с плавающей точкой.
%6f – печать числа с плавающей точкой в поле из шести позиций.
%.2f – печать числа с плавающей точкой с двумя цифрами после десятичной точки.
%6.2f - печать числа с плавающей точкой и двумя цифрами после десятичной точки в поле из шести позиций.
Кроме того, printf допускает следующие спецификаторы: %o для восьмеричного числа; %x для шестнадцатеричного числа; %c для символа; %s для строки символов и %% для самого %.

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

Упражнение 1.4. Напишите программу, которая будет печатать таблицу соответствия температур по Цельсию температурам по Фаренгейту.


Побитовые операторы


В Си имеются шесть операторов для манипулирования с битами. Их можно применять только к целочисленным операндам, т. е. к операндам типов char, short, int и long, знаковым и беззнаковым.

& - побитовое И | - побитовое ИЛИ ^ - побитовое исключающее ИЛИ. > - сдвиг вправо. ~ - побитовое отрицание (унарный).

Оператор & (побитовое И) часто используется для обнуления некоторой группы разрядов. Например

n = n & 0177;

обнуляет в n все разряды, кроме младших семи.

Оператор | (побитовое ИЛИ) применяют для установки разрядов; так,

x = x | SET_ON;

устанавливает единицы в тех разрядах x, которым соответствуют единицы в SET_ON.

Оператор ^ (побитовое исключающее ИЛИ) в каждом разряде установит 1, если соответствующие разряды операндов имеют различные значения, и 0, когда они совпадают.

Поразрядные операторы & и | следует отличать от логических операторов && и , которые при вычислении слева направо дают значение истинности. Например, если x равно 1, а y равно 2, то x & y даст нуль, а x && y - единицу.

Операторы и >> сдвигают влево или вправо свой левый операнд на число битовых позиций, задаваемое правым операндом, который должен быть неотрицательным. Так, x

Унарный оператор ~ поразрядно "обращает” целое т. е. превращает каждый единичный бит в нулевой и наоборот. Например

x = x & ~077

обнуляет в x последние 6 разрядов. Заметим, что запись x & ~077 не зависит от длины слова, и, следовательно, она лучше, чем x & 0177700, поскольку последняя подразумевает, что x занимает 16 битов. Не зависимая от машины форма записи ~077 не потребует дополнительных затрат при счете, так как ~077 - константное выражение, которое будет вычислено во время компиляции.

Для иллюстрации некоторых побитовых операций рассмотрим функцию getbits(x, p, n), которая формирует поле в n битов, вырезанных из x, начиная с позиции p, прижимая его к правому краю. Предполагается, что 0-й бит - крайний правый бит, а n и p- осмысленные положительные числа. Например, getbits(x,4,3) вернет в качестве результата 4, 3 и 2-й биты значения x, прижимая их к правому краю. Вот эта функция:

/* getbits: получает n бит, начиная с p-й позиции */ unsigned getbits(unsigned x, int p, int n) { return (x >> (p+1-n)) & ~(~0

Выражение x >> (р+1-n) сдвигает нужное нам поле к правому краю. Константа ~0 состоит из одних единиц, и ее сдвиг влево на n бит (~0

Упражнение 2.6. Напишите функцию setbits(x, p, n, y), возвращающую значение x, в котором n битов, начиная с p-й позиции, заменены на n правых разрядов из y (остальные биты не изменяются).

Упражнение 2.7. Напишите функцию invert(x, p, n), возвращающую значение x с инвертированными n битами, начиная с позиции p (остальные биты не изменяются).

Упражнение 2.8. Напишите функцию rightrot (x, n), которая циклически сдвигает x вправо на n разрядов.



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


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

#include <stdio.h>

/* подсчет вводимых символов; 1-я версия */ main() { long nc; nc = 0;

while (getchar() != EOF) ++nc; printf(“%ld\n”, nc); }

Инструкция

++nc;

представляет новый оператор ++, который означает увеличить на единицу. Вместо этого можно было бы написать nc=nc+1, но ++nc намного короче, а часто и эффективнее. Существует аналогичный оператор --, означающий уменьшить на единицу. Операторы ++ и -- могут быть как префиксными (++nc), так и постфиксными (nc++). Как будет показано в , эти две формы в выражениях имеют разные значения, но и ++nc, и nc++

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

Программа подсчета символов накапливает сумму в переменной типа long. Целые типа long имеют не менее 32 битов. Хотя на некоторых машинах типы int и long имеют одинаковый размер, существуют, однако, машины, в которых int занимает 16 бит с максимально возможным значением 32767, а это - сравнительно маленькое число, и счетчик типа int может переполниться. Спецификация %ld в printf указывает, что соответствующий аргумент имеет тип long.

Возможно охватить еще больший диапазон значений, если использовать тип double (т. е. float с двойной точностью). Применим также инструкцию for вместо while, чтобы продемонстрировать другой способ написания цикла.

#include <stdio.h>

/* подсчет вводимых символов; 2-й версия */ main() { double nc; for (nc = 0; getchar() != EOF; ++nc) ; printf(“%.0f\n”, nc); }

В printf спецификатор %f применяется как для float, так и для double; спецификатор %.0f означает печать без десятичной точки и дробной части (последняя в нашем случае отсутствует).

Тело указанного for-цикла пусто, поскольку кроме проверок и приращений счетчика делать ничего не нужно. Но правила грамматики Си требуют, чтобы for- цикл имел тело. Выполнение этого требования обеспечивает изолированная точка с запятой, называемая пустой инструкцией. Мы поставили точку с запятой на отдельной строке для большей наглядности.

Наконец, заметим, что если ввод не содержит ни одного символа, то при первом же обращении к getchar условие в while или for не будет выполнено и программа выдаст нуль, что и будет правильным результатом. Это важно. Одно из привлекательных свойств циклов while и for состоит в том, что условие проверяется до того, как выполняется тело цикла. Если ничего делать не надо, то ничего делаться и не будет, пусть даже тело цикла не выполнится ни разу. Программа должна вести себя корректно и при нулевом количестве вводимых символов. Само устройство циклов while и for дает дополнительную уверенность в правильном поведении программы в случае граничных условий.



Подсчет слов


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

#include <stdio.h>

#define IN 1 /* внутри слова */ #define OUT 0 /* вне слова */

/* подсчет строк, слов и символов */ main() { int с, nl, nw, nc, state; state = OUT; nl = nw = nc = 0; while ((с = getchar()) != EOF) { ++nc; if (c == '\n') ++nl; if (c == ' ' c == '\n' c == '\t') state = OUT; else if (state == OUT) { state = IN; ++nw; } } printf(“%d %d %d\n”, nl, nw, nc); }

Каждый раз, встречая первый символ слова, программа изменяет значение счетчика слов на 1. Переменная state фиксирует текущее состояние - находимся мы внутри или вне слова. Вначале ей присваивается значение OUT, что соответствует состоянию “вне слова”. Мы предпочитаем пользоваться именованными константами IN и OUT, а не собственно значениями 1 и 0, чтобы сделать программу более понятной. В такой маленькой программе этот прием мало что дает, но в большой программе увеличение ее ясности окупает незначительные дополнительные усилия, потраченные на то, чтобы писать программу в таком стиле с самого начала. Вы обнаружите, что большие изменения гораздо легче вносить в те программы, в которых магические числа встречаются только в виде именованных констант.

Строка

nl = nw = nc = 0;

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

nl = (nw = (nc = 0));

Оператор означает ИЛИ, так что строка

if (c == ' ' c == '\n' c == '\t' )

читается как “если c есть пробел, или c есть новая строка, или c есть табуляция”. (Напомним, что видимая эскейп-последовательность \t обозначает символ табуляции.) Существует также оператор &&, означающий И. Его приоритет выше, чем приоритет . Выражения, связанные операторами && или , вычисляются слева направо; при этом гарантируется, что вычисления сразу прервутся, как только будет установлена истинность или ложность условия. Если c есть пробел, то дальше проверять, является значение c символом новой строки или же табуляции, не нужно. В этом частном случае данный способ вычислений не столь важен, но он имеет значение в более сложных ситуациях, которые мы вскоре рассмотрим.


В примере также встречается слово else, которое указывает на альтернативные действия, выполняемые в случае, когда условие, указанное в if, не является истинным. В общем виде условная инструкция записывается так:

if (выражение) инструкция1 else

инструкция2

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

Упражнение 1.11. Как протестировать программу подсчета слов? Какой ввод вероятнее всего обнаружит ошибки, если они были допущены?

Упражнение 1.12. Напишите программу, которая печатает содержимое своего ввода, помещая по одному слову на каждой строке.


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


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

#include <stdio.h> /* подсчет строк входного потока */ main() { int c, nl; nl = 0; while ((с = getchar()) != EOF) if (c == '\n') ++nl; printf(“%d\n”, nl); }

Тело цикла теперь образует инструкция if, под контролем которой находится увеличение счетчика nl на единицу. Инструкция if проверяет условие в скобках и, если оно истинно, выполняет следующую за ним инструкцию (или группу инструкций, заключенную в фигурные скобки). Мы опять делаем отступы в тексте программы, чтобы показать, что чем управляется.

Двойной знак равенства в языке Си обозначает оператор “равно” (он аналогичен оператору = в Паскале и .EQ. в Фортране). Удваивание знака = в операторе проверки на равенство сделано для того, чтобы отличить его от единичного =, используемого в Си для обозначения присваивания. Предупреждаем: начинающие программировать на Си иногда пишут =, а имеют в виду ==. Как мы увидим в , в этом случае результатом будет обычно вполне допустимое по форме выражение, на которое компилятор не выдаст никаких предупреждающих сообщений (Современные компиляторы, как правило, выдают предупреждение о возможной ошибке. – Примеч. ред.).

Символ, заключенный в одиночные кавычки, представляет собой целое значение, равное коду этого символа (в кодировке, принятой на данной машине). Это так называемая символьная константа. Существует и другой способ для написания маленьких целых значений. Например, 'A' есть символьная константа, в наборе символов ASCII ее значение равняется 65 - внутреннему представлению символа A. Конечно, 'A' в роли константы предпочтительнее, чем 65, поскольку смысл первой записи более очевиден, и она не зависит от конкретного способа кодировки символов.


Эскейп-последовательности, используемые в строковых константах, допускаются также и в символьных константах. Так, '\n' обозначает код символа новой строки, который в ASCII равен 10. Следует обратить особое внимание на то, что '\n' обозначает один символ (код которого в выражении рассматривается как целое значение), в то время как “\n” - строковая константа, в которой чисто случайно указан один символ. Более подробно различие между символьными и строковыми константами разбирается в .

Упражнение 1.8. Напишите программу для подсчета пробелов, табуляций и новых строк.

Упражнение 1.9. Напишите программу, копирующую символы ввода в выходной поток и заменяющую стоящие подряд пробелы на один пробел.

Упражнение 1.10. Напишите программу, копирующую вводимые символы в выходной поток с заменой символа табуляции на \t, символа забоя на \b и каждой обратной наклонной черты на \\. Это сделает видимыми все символы табуляции и забоя.


в мире компьютеров произошла революция.


С момента публикации в 1978 г. книги "Язык программирования Си" в мире компьютеров произошла революция. Большие машины стали еще больше, а возможности персональных ЭВМ теперь сопоставимы с возможностями больших машин десятилетней давности. Язык Си за это время также изменился, хотя и не очень сильно; что же касается сферы применения Си, то она далеко вышла за рамки его начального назначения как инструментального языка операционной системы UNIX.
Рост популярности Си, накапливающиеся с годами изменения, создание компиляторов коллективами разработчиков, ранее не причастных к проектированию языка, - все это послужило стимулом к более точному и отвечающему времени определению языка по сравнению с первым изданием книги. В 1983 г. Американский институт национальных стандартов (American National Standards Institute - ANSI) учредил комитет, перед которым была поставлена цель выработать "однозначное и машинно-независимое определение языка Си", полностью сохранив при этом его стилистику. Результатом работы этого комитета и явился стандарт ANSI языка Си.
Стандарт формализует средства языка, которые в первом издании были только намечены, но не описаны, такие, например, как присваивание структурам и перечисления. Он вводит новый вид описания функций, позволяющий проводить повсеместную проверку согласованности вызовов функций с их определением; специфицирует стандартную библиотеку с широким набором функций ввода-вывода, управления памятью, манипуляций со строками символов и другими функциями; уточняет семантику, бывшую в первоначальном определении неясной, и явно выделяет то, что остается машинно-зависимым.
Во втором издании книги "Язык программирования Си" представлена версия Си, принятая в качестве стандарта ANSI. Мы решили описать язык заново, отметив при этом те места, в которых он претерпел изменения. В большинство параграфов это не привнесло существенных перемен, самые заметные различия касаются новой формы описания и определения функции. Следует отметить, что современные компиляторы уже обеспечили поддержку значительной части стандарта.


Мы попытались сохранить краткость первого издания. Си - небольшой язык, и чтобы его описать большой книги не требуется. В новом издании улучшено описание наиболее важных средств, таких как указатели, которые занимают центральное место в программировании на Си; доработаны старые примеры, а в некоторые главы добавлены новые. Так, для усиления трактовки сложных объявлений в качестве примеров включены программы перевода объявлений в их словесные описания и обратно. Как и раньше, все примеры были протестированы прямо по текстам, написанным в воспринимаемой машиной форме.
- это справочное руководство, но отнюдь не стандарт. В нем мы попытались уложить самое существенное на минимуме страниц. По замыслу это приложение должно легко читаться программистом- пользователем: для разработчиков же компилятора определением языка должен служить сам стандарт. В приведены возможности стандартной библиотеки. Оно также представляет собой справочник для прикладных программистов, но не для разработчиков компиляторов. содержит краткий перечень отличий представленной версии языка Си от его начальной версии.
В предисловии к первому изданию мы говорили о том, что "чем больше работаешь с Си, тем он становится удобнее". Это впечатление осталось и после десяти лет работы с ним. Мы надеемся, что данная книга поможет вам изучить Си и успешно его использовать.
Мы в большом долгу перед друзьями, которые помогали нам в выпуске второго издания книги. Джон Бентли, Дуг Гунн, Дуг Макилрой, Питер Нельсон и Роб Пайк сделали четкие замечания почти по каждой странице первого варианта рукописи. Мы благодарны Алу Ахо, Деннису Аллиссону, Джою Кемпбеллу, Г. Р. Эмлину, Карен Фортганг, Аллену Голубу, Эндрю Хьюму, Дэйву Кристолу, Джону Линдерману, Дэйву Проссеру, Гину Спаффорду и Крису Ван Уику за внимательное прочтение книги. Мы получили полезные советы от Билла Чезвика, Марка Кернигана, Эндрю Коэнига, Робина Лейка, Тома Лондона, Джима Ридза, Кловиза Тондо и Питера Вайнбергера. Дейв Проссер ответил на многочисленные вопросы, касающиеся деталей стандарта ANSI. Мы широко пользовались транслятором с Си++ Бьерна Страуструпа для локальной проверки наших программ, а Дейв Кристол предоставил нам ANSI Си-компилятор для окончательной их проверки. Рич Дрешлер очень помог в наборе книги. Мы искренне благодарим всех.
Брайан В. Керниган, Деннис М. Ритчи

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


Си — это универсальный язык программирования с компактным способом записи выражений, современными механизмами управления структурами данных и богатым набором операторов. Си не является ни языком "очень высокого уровня", ни "большим" языком, не рассчитан он и на какую-то конкретную область применения. Однако благодаря широким возможностям и универсальности для решения многих задач он удобнее и эффективнее, чем предположительно более мощные языки.
Первоначально Си был создан Деннисом Ритчи как инструмент написания операционной системы UNIX для машины PDP-11 и реализован в рамках этой операционной системы. И операционная система, и Си- компилятор, и, по существу, все прикладные программы системы UNIX (включая и те, которые использовались для подготовки текста этой книги - Имеется в виду оригинал этой книги на английском языке. – Примеч. пер. ) написаны на Си. Фирменные Си-компиляторы существуют и на нескольких машинах других типов, среди которых 1ВМ/370, Honeywell 6000 и Interdata 8/32. Си не привязан к конкретной аппаратуре или системе, однако на нем легко писать программы, которые без каких-либо изменений переносятся на другие машины, где осуществляется его поддержка.
Цель нашей книги - помочь читателю научиться программировать на Си. Издание включает введение- учебник, позволяющий новичкам начать программировать как можно скорее, а также главы, посвященные основным свойствам языка, и справочное руководство. В ее основу положены изучение, написание и проработка примеров, а не простое перечисление правил. Почти все наши примеры — это законченные реальные программы, а не разобщенные фрагменты. Все они были оттестированы на машине точно в том виде, как приводятся в книге. Помимо демонстрации эффективного использования языка, там, где это было возможно, мы стремились проиллюстрировать полезные алгоритмы и принципы хорошего стиля написания программ и их разумного проектирования.
Эта книга не является вводным курсом по программированию. Предполагается, что читатель знаком с такими основными понятиями, как "переменная", "присваивание", "цикл", "функция". Тем не менее и новичок сможет изучить язык, хотя для него будет очень полезным общение с более знающими специалистами.


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

Предметный указатель


0... константа восьмеричная 2.3, A2.5.1

0x... константа шестнадцатеричная 2.3, A2.5.1

... многоточие , A7.3.2

>= оператор больше или равно 2.6,

> оператор больше чем 2.6,

- оператор вычитания ,

-- оператор декремента 1.5.2, 2.8, , , A7.4.1

/ оператор деления , 2.5,

% оператор деления по модулю 2.5,

. оператор доступа к элементу структуры 6.1, , A7.3.3

-> оператор доступа к элементу структуры через указатель , A7.3, A7.3.3

, оператор запятая , A7.18

++ оператор инкремента 1.5.2, 2.8, , , A7.4.1

* оператор косвенного доступа 5.1, A7.4.3

&& оператор логического И 1.5.4, 2.6, ,

оператор логического ИЛИ 1.5.4, 2.6, ,

! оператор логического отрицания 2.6, , A7.4.7

<= оператор меньше или равно 2.6,

< оператор меньше чем 2.6,

!= оператор неравенства 1.5.1, 2.6, A7.10

& оператор побитового И 2.9, A7.11

| оператор побитового ИЛИ 2.9, A7.13

^ оператор побитового исключающего ИЛИ 2.9, A7.12

~ оператор побитового отрицания 2.9, A7.4.6

& оператор получения адреса 5.1, A7.4.2

# оператор препроцессора 4.11.2, A12.3

## оператор препроцессора 4.11.2, A12.3

= оператор присваивания 1.5.1, 2.6, A7.17

+= оператор присваивания 2.10

== оператор равенства 1.5.3, 2.6, A7.10

<< оператор сдвига влево 2.9,

>> оператор сдвига вправо 2.9,

+ оператор сложения ,

* оператор умножения ,

- оператор унарный минус A7.4.5

+ оператор унарный плюс A7.4.4

" символ двойная кавычка 1.1, 1.5.3, 2.3, A2.5.2, A2.6

' символ кавычка , , A2.5.2

\0 символ нулевой , 2.3, A2.5.2

\\ символ обратная наклонная черта 1.1, 2.3

_ символ подчеркивания 2.1, , B

?: условное выражение 2.11, A7.16



Преобразования типов


Если операнды оператора принадлежат к разным типам, то они приводятся к некоторому общему типу. Приведение выполняется в соответствии с небольшим числом правил. Обычно автоматически производятся лишь те преобразования, которые без какой-либо потери информации превращают операнды с меньшим диапазоном значений в операнды с большим диапазоном, как, например, преобразование целого в число с плавающей точкой в выражении вроде f+i. Выражения, не имеющие смысла, например число с плавающей точкой в роли индекса, не допускаются. Выражения, в которых могла бы теряться информация (скажем, при присваивании длинных целых переменным более коротких типов или при присваивании значений с плавающей точкой целым переменным), могут повлечь за собой предупреждение, но они допустимы.

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

/* atoi: преобразование s в целое */ int atoi(char s[]) { int i, n; n = 0; for (i = 0; s[i] >= '0' && s[i]

Как мы уже говорили в , выражение

s[i] -'0'

дает числовое значение символа, хранящегося в s[i], так как значения '0', '1' и пр. образуют непрерывную возрастающую последовательность.

Другой пример приведения char к int связан с функцией lower, которая одиночный символ из набора ASCII, если он является заглавной буквой, превращает в строчную. Если же символ не является заглавной буквой, lower его не изменяет.

/* lower: преобразование c в строчную, только для ASCII */ int lower(int c) { if (c >= 'A' && c

В случае ASCII эта программа будет работать правильно, потому что между одноименными буквами верхнего и нижнего регистров - одинаковое расстояние (если их рассматривать как числовые значения). Кроме того, латинский алфавит - плотный, т. е. между буквами A и Z расположены только буквы. Для набора EBCDIC последнее условие не выполняется, и поэтому наша программа в этом случае будет преобразовывать не только буквы.


Стандартный заголовочный файл <ctype.h>, описанный в , определяет семейство функций, которые позволяют проверять и преобразовывать символы независимо от символьного набора. Например, функция tolower(c) возвращает букву c в коде нижнего регистра, если она была в коде верхнего регистра, поэтому tolower - универсальная замена функции lower, рассмотренной выше. Аналогично проверку

c >= '0' && c

можно заменить на isdigit(c)

Далее мы будем пользоваться функциями из <ctype.h>.

Существует одна тонкость, касающаяся преобразования символов в целые числа: язык не определяет, являются ли переменные типа char знаковыми или беззнаковыми. При преобразовании char в int может ли когда- нибудь получиться отрицательное целое? На машинах с разной архитектурой ответы могут отличаться. На некоторых машинах значение типа char с единичным старшим битом будет превращено в отрицательное целое (посредством "распространения знака”). На других - преобразование char в int осуществляется добавлением нулей слева, и, таким образом, получаемое значение всегда положительно.

Гарантируется, что любой символ из стандартного набора печатаемых символов никогда не будет отрицательным числом, поэтому в выражениях такие символы всегда являются положительными операндами. Но произвольный восьмибитовый код в переменной типа char на одних машинах может быть отрицательным числом, а на других - положительным. Для совместимости переменные типа char, в которых хранятся несимвольные данные, следует специфицировать явно как signed или unsigned.

Отношения вроде i > j и логические выражения, перемежаемые операторами && и , определяют выражение-условие, которое имеет значение 1, если оно истинно, и 0, если ложно. Так, присваивание

d = c >= '0' && c

установит d в значение 1, если c есть цифра, и 0 в противном случае. Однако функции, подобные isdigit, в качестве истины могут выдавать любое ненулевое значение. В местах проверок внутри if, while, for и пр. "истина” просто означает "не нуль”.



Неявные арифметические преобразования, как правило, осуществляются естественным образом. В общем случае, когда оператор вроде + или * с двумя операндами (бинарный оператор) имеет разнотипные операнды, прежде чем операция начнет выполняться, "низший” тип повышается до "высшего”. Результат будет иметь высший тип. В приложения A правила преобразования сформулированы точно. Если же в выражении нет беззнаковых операндов, можно удовлетвориться следующим набором неформальных правил:

Если какой-либо из операндов принадлежит типу long double, то и другой приводится к long double. В противном случае, если какой-либо из операндов принадлежит типу double, то и другой приводится к double. В противном случае, если какой-либо из операндов принадлежит типу float, то и другой приводится к float. В противном случае операнды типов char и short приводятся к int. И наконец, если один из операндов типа long, то и другой приводится к long.

Заметим, что операнды типа float не приводятся автоматически к типу double; в этом данная версия языка отличается от первоначальной. Вообще говоря, математические функции, аналогичные собранным в библиотеке <math.h>, базируются на вычислениях с двойной точностью. В основном float используется для экономии памяти на больших массивах и не так часто - для ускорения счета на тех машинах, где арифметика с двойной точностью слишком дорога с точки зрения расхода времени и памяти.

Правила преобразования усложняются с появлением операндов типа unsigned. Проблема в том, что сравнения знаковых и беззнаковых значений зависят от размеров целочисленных типов, которые на разных машинах могут отличаться. Предположим, что значение типа int занимает 16 битов, а значение типа long - 32 бита. Тогда -1L < 1U, поскольку 1U принадлежит типу unsigned int и повышается до типа signed long. Но –1L >1UL, так как -1L повышается до типа unsigned long и воспринимается как большое положительное число.

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



Тип char превращается в int путем распространения знака или другим описанным выше способом.

Тип long int преобразуются в short int или в значения типа char путем отбрасывания старших разрядов. Так, в

int i; char c; i = c; c = i;

значение c не изменится. Это справедливо независимо от того, распространяется знак при переводе char в int или нет. Однако, если изменить очередность присваиваний, возможна потеря информации.

Если x принадлежит типу float, а i - типу int, то и x=i, и i=z вызовут преобразования, причем перевод float в int сопровождается отбрасыванием дробной части. Если double переводится во float, то значение либо округляется, либо обрезается; это зависит от реализации.

Так как аргумент в вызове функции есть выражение, при передаче его функции также возможно преобразование типа. При отсутствии прототипа (функции аргументы тина char и short переводятся в int, a float - в double. Вот почему мы объявляли аргументы типа int или double даже тогда, когда в вызове функции использовали аргументы типа char или float.

И наконец, для любого выражения можно явно ("насильно”) указать преобразование его типа, используя унарный оператор, называемый приведением. Конструкция вида

(имя-типа) выражение

приводит выражение к указанному в скобках типу по перечисленным выше правилам. Смысл операции приведения можно представить себе так: выражение как бы присваивается некоторой переменной указанного типа, и эта переменная используется вместо всей конструкции. Например, библиотечная функция sqrt рассчитана на аргумент типа double и выдает чепуху, если ей подсунуть что-нибудь другое (sqrt описана в ). Поэтому, если n имеет целочисленный тип, мы можем написать

sqrt((double) n)

и перед тем, как значение n будет передано функции, оно будет переведено в double. Заметим, что операция приведения всего лишь вырабатывает значение n указанного типа, но саму переменную n не затрагивает. Приоритет оператора приведения столь же высок, как и любого унарного оператора, что зафиксировано в таблице, помещенной в конце этой главы.



В том случае, когда аргументы описаны в прототипе функции, как тому и следует быть, при вызове функции нужное преобразование выполняется автоматически. Так, при наличии прототипа функции sqrt:

double sqrt(double);

перед обращением к sqrt в присваивании

root2 = sqrt(2);

целое 2 будет переведено в значение double 2.0 автоматически без явного указания операции приведения.

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

unsigned long int next = 1; /* rand: возвращает псевдослучайное целое 0...32767 */ int rand(void) { next = next * 1103515245 + 12345; return (unsigned int)(next/65536) % 32768; }

/* srand: устанавливает "семя” для rand() */ void srand(unsigned int seed) { next = seed; }

Упражнение 2.3. Напишите функцию htol(s), которая преобразует последовательность шестнадцатеричных цифр, начинающуюся с 0x или 0X, в соответствующее целое. Шестнадцатеричными цифрами являются символы 0...9, a...f, А...F.


Препроцессор языка Си


Некоторые возможности языка Си обеспечиваются препроцессором, который работает на первом шаге компиляции. Наиболее часто используются две возможности: #include, вставляющая содержимое некоторого файла во время компиляции, и #define, заменяющая одни текстовые последовательности на другие. В этом параграфе обсуждаются условная компиляция и макроподстановка с аргументами.



B. Стандартная библиотека




Настоящее приложение представляет собой краткое изложение библиотеки, утвержденной в качестве ANSI-стандарта. Сама по себе библиотека не является частью языка, однако, заложенный в ней набор функций, а также определений типов и макросов составляет системную среду, поддерживающую стандарт Си. Мы не приводим здесь несколько функций с ограниченной областью применения – те, которые легко синтезируются из других функций, а также опускаем все то, что касается многобайтовых символов и специфики, обусловленной языком, национальными особенностями и культурой.

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

<assert.h> <ctype.h> <errno.h>

<float.h> <limits.h> <locale.h>

<math.h> <setjmp.h> <signal.h>

<stdarg.h> <stddef.h> <stdio.h>

<stdlib.h> <string.h> <time.h>

Доступ к заголовочному файлу осуществляется с помощью строки препроцессора

#include <заголовочный файл>

Заголовочные файлы можно включать в любом порядке и сколько угодно раз. Строка #include не должна быть внутри внешнего объявления или определения и должна встретиться раньше, чем что-нибудь из включаемого заголовочного файла будет востребовано. В конкретной реализации заголовочный файл может и не быть исходным файлом.

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

Определенные в <stdio.h> функции ввода-вывода, а также типы и макросы составляют приблизительно одну треть библиотеки.

Поток - это источник или получатель данных; его можно связать с диском или с каким-то другим внешним устройством. Библиотека поддерживает два вида потоков: текстовый и бинарный, хотя на некоторых системах, в частности в UNIXe, они не различаются. Текстовый поток - это последовательность строк; каждая строка имеет нуль или более символов и заканчивается символом '\n'. Операционная среда может потребовать коррекции текстового потока (например, перевода '\n' в символы возврат-каретки и перевод-строки).


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

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

Когда программа начинает работу, уже открыты три потока: stdin, stdout и stderr.

Ниже перечислены функции, оперирующие с файлами. Тип size_t - беззнаковый целочисленный тип, используемый для описания результата оператора sizeof.

FILE *fopen(const char *filename, const char *mode);

fopen открывает файл с заданным именем и возвращает поток или NULL, если попытка открытия оказалась неудачной. Режим mode допускает следующие значения:

"r" - текстовый файл открывается для чтения (от read (англ.) - читать);
"w" - текстовый файл создается для записи; старое содержимое (если оно было) выбрасывается (от write (англ.) - писать);
"а" - текстовый файл открывается или создается для записи в конец файла (от append (англ.) - добавлять);
"r+" - текстовый файл открывается для исправления (т. е. для чтения и для записи);
"w+" - текстовый файл создается для исправления; старое содержимое (если оно было) выбрасывается;
"a+" - текстовый файл открывается или создается для исправления уже существующей информации и добавления новой в конец файла.
Режим "исправления" позволяет читать и писать в один и тот же файл; при переходах от операций чтения к операциям записи и обратно должны осуществляться обращения к fflush или к функции позиционирования файла. Если указатель режима дополнить буквой b (например "rb" или "w+b"), то это будет означать, что файл бинарный. Ограничение на длину имени файла задано константой FILENAME_MAX. Константа FOPEN_MAX ограничивает число одновременно открытых файлов.



FILE *freopen(const char *filename, const char *mode, FILE *stream);

freopen открывает файл с указанным режимом и связывает его с потоком stream. Она возвращает stream или, в случае ошибки, NULL. Обычно freopen используется для замены файлов, связанных с stdin, stdout или stderr, другими файлами.

int fflush(FILE *stream);

Применяемая к потоку вывода функция fflush производит дозапись всех оставшихся в буфере (еще не записанных) данных, для потока ввода эта функция не определена. Возвращает EOF в случае возникшей при записи ошибки или нуль в противном случае. Обращение вида fflush(NULL) выполняет указанные операции для всех потоков вывода.

int fclose(FILE *stream);

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

int remove(const char *filename);

remove удаляет файл с указанным именем; последующая попытка открыть файл с этим именем вызовет ошибку. Возвращает ненулевое значение в случае неудачной попытки.

int rename(const char *oldname, const char *newname);

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

FILE *tmpfile(void);

tmpfile создает временный файл с режимом доступа "wb+", который автоматически удаляется при его закрытии или обычном завершении программой своей работы. Эта функция возвращает поток или, если не смогла создать файл, NULL.

char *tmpnam(char s[L_tmpnam]);

tmpnam(NULL) создает строку, не совпадающую ни с одним из имен существующих файлов, и возвращает указатель на внутренний статический массив. tmpnam(s) запоминает строку в s и возвращает ее в качестве значения функции; длина s должна быть не менее L_tmpnam. При каждом вызове tmpnam генерируется новое имя; при этом гарантируется не более TMPMAX различных имен за один сеанс работы программы. Заметим, что tmpnam создает имя, а не файл.



int setvbuf(FILE *stream, char *buf, int mode, size_t size);

setvbuf управляет буферизацией потока; к ней следует обращаться прежде, чем будет выполняться чтение, запись или какая-либо другая операция, mode со значением _IOFBF вызывает полную буферизацию, с _IOLBF - "построчную" буферизацию текстового файла, a mode со значением _IONBF отменяет всякую буферизацию. Если параметр buf не есть NULL, то его значение - указатель на буфер, в противном случае под буфер будет запрашиваться память. Параметр size задает размер буфера. Функция setvbuf в случае ошибки выдает ненулевое значение.

void setbuf(FILE *stream, char *buf);

Если buf есть NULL, то для потока stream буферизация выключается. В противном случае вызов setbuf приведет к тем же действиям, что и вызов (void) setvbuf (stream, buf, _IOFBF, BUFSIZ).

Функции printf осуществляют вывод информации по формату.

int fprintf(FILE *stream, const char *format, ...);

fprintf преобразует и пишет вывод в поток stream под управлением format. Возвращаемое значение - число записанных символов или, в случае ошибки, отрицательное значение.

Форматная строка содержит два вида объектов: обычные символы, копируемые в выводной поток, и спецификации преобразования, которые вызывают преобразование и печать остальных аргументов в том порядке, как они перечислены. Каждая спецификация преобразования начинается с % и заканчивается символом-спецификатором преобразования. Между % и символом- спецификатором в порядке, в котором они здесь перечислены, могут быть расположены следующие элементы информации:

Флаги (в любом порядке), модифицирующие спецификацию:

- - указывает на то, что преобразованный аргумент должен быть прижат к левому краю поля;
+ - предписывает печатать число всегда со знаком;
пробел - если первый символ - не знак, то числу должен предшествовать пробел;
0 - указывает, что числа должны дополняться слева нулями до всей ширины поля;
# - указывает на одну из следующих форм вывода: для o первой цифрой должен быть 0; для x или X ненулевому результату должны предшествовать 0x или 0X; для e, E, f, g и G вывод должен обязательно содержать десятичную точку; для g и G завершающие нули не отбрасываются.
<


/li>

Число, специфицирующее минимальную ширину поля. Преобразованный аргумент будет напечатан в поле, размер которого не меньше указанной ширины, а если потребуется, в поле большего размера. Если число символов преобразованного аргумента меньше ширины поля, то поле будет дополнено слева (или справа, если число прижимается к левому краю). Обычно поле дополняется пробелами (или нулями, если присутствует флаг дополнения нулями). Точка, отделяющая указатель ширины поля от указателя точности. Число, задающее точность, которое специфицирует максимальное количество символов, печатаемых из строки, или количество цифр после десятичной точки в преобразованиях e, E или f, или количество значащих цифр для g или G - преобразования, или минимальное количество цифр при печати целого (до необходимой ширины поля число дополняется слева нулями). Модификаторы h, l (буква ell) или L. "h" указывает на то, что соответствующий аргумент должен печататься как short или unsigned short; "l" сообщает, что аргумент имеет тип long или unsigned long; "L" информирует, что аргумент принадлежит типу long double.

Ширина, или точность, или обе эти характеристики могут быть специфицированы с помощью *; в этом случае необходимое число "извлекается" из следующего аргумента, который должен иметь тип int (в случае двух звездочек используются два аргумента).

Символы-спецификаторы и разъяснение их смысла приведены в . Если за % нет правильного символа-спецификатора, результат не определен.

int printf(const char *format, ...);

printf(...) полностью эквивалентна fprintf(stdout, ...).

int sprintf(char *s, const char *format, ...)

sprintf действует так же, как и printf, только вывод осуществляет в строку s, завершая ее символом '\0'. Строка s должна быть достаточно большой, чтобы вмещать результат вывода. Возвращает количество записанных символов, в число которых символ '\0' не входит.

int vprintf (const char *format, va_list arg) int vfprintf (FILE *stream, const char *format, va_list arg) int vsprintf (char *s, const char *format, va_list arg)



Функции vprintf, vfprintf и vsprintf эквивалентны соответствующим printf- функциям с той лишь разницей, что переменный список аргументов представлен параметром arg, инициализированным макросом va_start и, возможно, вызовами va_arg (см. в B7 описание <stdarg.h>).

Таблица B-1. Преобразования printf

Символ

Тип аргумента; вид печати
d, i int; знаковая десятичная запись
o unsigned int; беззнаковая восьмеричная запись (без 0 слева)
x, X unsigned int; беззнаковая шестнадцатеричная запись (без 0x или 0X слева), в качестве цифр от 10 до 15 используются abcdef для x и ABCDEF для X
u unsigned int; беззнаковое десятичное целое
c int; единичный символ после преобразования в unsigned char
s char *; символы строки печатаются, пока не встретится '\0' или не исчерпается количество символов, указанное точностью
f double; десятичная запись вида [-]mmm.ddd, где количество d специфицируется точностью. По умолчанию точность равна 6; нулевая точность подавляет печать десятичной точки
e, E double; десятичная запись вида [-]m.dddddde±xx или запись вида [-]m.ddddddE±xx, где количество d специфицируется точностью. По умолчанию точность равна 6; нулевая точность подавляет печать десятичной точки
g, G double; используется %e и %E, если порядок меньше -4 или больше или равен точности; в противном случае используется %f. Завершающие нули и точка в конце не печатаются
p void *; печатает в виде указателя (представление зависит от реализации
n int *; число символов, напечатанных к данному моменту данным вызовом printf, записывается в аргумент. Никакие другие аргументы не преобразуются
% никакие аргументы не преобразуются; печатается %
Функции scanf имеют дело с форматным преобразованием при вводе

int fscanf(FILE *stream, const char *format, ...);

fscanf читает данные из потока stream под управлением format и преобразованные величины присваивает по порядку аргументам, каждый из которых должен быть указателем. Завершает работу, если исчерпался формат. Выдает EOF по исчерпании файла или перед любым преобразованием, если возникла ошибка; в остальных случаях функция возвращает количество преобразованных и введенных элементов.



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

пробелы и табуляции, которые игнорируются; обычные символы (кроме %), которые ожидаются в потоке ввода среди символов, отличных от символов-разделителей; спецификации преобразования, состоящие из %; необязательного знака *, подавляющего присваивание; необязательного числа, специфицирующего максимальную ширину поля; необязательных h, l или L, указывающих размер присваиваемого значения, и символа-спецификатора преобразования.

Спецификация преобразования определяет преобразование следующего поля ввода. Обычно результат размещается в переменной, на которую указывает соответствующий аргумент. Однако если присваивание подавляется с помощью знака *, как, например, в %*s, то поле ввода просто пропускается, и никакого присваивания не происходят. Поле ввода определяется как строка символов, отличных от символов-разделителей; при этом ввод строки прекращается при выполнении любого из двух условий: если встретился символ-разделитель или если ширина поля (в случае, когда она указана) исчерпана. Из этого следует, что при переходе к следующему полю scanf может "перешагивать" через границы строк, поскольку символ новой строки является символом-разделителем. (Под символами-разделителями понимаются символы пробела, табуляции, новой строки, возврата каретки, вертикальной табуляции и смены страницы.)

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

Символам-спецификаторам d, i, n, o, u и x может предшествовать h, если аргумент есть указатель на short (а не int) или l (буква ell), если аргумент есть указатель на long. Символам-спецификаторам e, f и g может предшествовать l, если аргумент - указатель на double (а не float), или L, если аргумент - указатель на long double.

int scanf (const char *format, ...);



scanf(...) делает то же, что и fscanf(stdin, ...).

int sscanf (const char *s, const char *format, ...);

sscanf(s, ...) делает то же, что и scanf(...), только ввод символов осуществляет из строки s.

Таблица B-2. Преобразования scanf

Символ

Данные на вводе; тип аргумента
d десятичное целое; int *
i целое: int *. Целое может быть восьмеричным (с нулем слева) или шестнадцатеричным (с 0x или 0X слева)
o восьмеричное целое (с нулем слева или без него); int *
u беззнаковое десятичное целое; unsigned int *
x шестнадцатеричное целое (с 0x или 0X слева или без них): int *
c символы, char *. Символы ввода размещаются в указанном массиве в количестве, заданном шириной поля; по умолчанию это количество равно 1. Символ'\0' не добавляется. Символы-разделители здесь рассматриваются как обычные символы и поступают в аргумент. Чтобы прочесть следующий символ-разделитель, используйте %1s
s строка символов, отличных от символов-разделителей (записывается без кавычек); char *, указывающий на массив размера достаточного, чтобы вместить строку и добавляемый к ней символ '\0'
e, f, g число с плавающей точкой; float *. Формат ввода для float состоит из необязательного знака, строки цифр, возможно с десятичной точкой, и необязательного порядка, состоящего из E или e и целого, возможно со знаком
p значение указателя в виде, в котором printf ("%р") его напечатает; void *
n записывает в аргумент число символов, прочитанных к этому моменту в этом вызове; int *. Никакого чтения ввода не происходит. Счетчик числа введенных элементов не увеличивается
[...] выбирает из ввода самую длинную непустую строку, состоящую из символов, заданных в квадратных скобках: char *. В конец строки добавляется '\0'. Спецификатор вида []...] включает ] в задаваемое множество символов
[^...] выбирает из ввода самую длинную непустую строку, состоящую из символов, не входящих в заданное в скобках множество. В конец добавляется '\0'. Спецификатор вида [^]...] включает ] в задаваемое множество символов
% обычный символ %; присваивание не делается
<


int fgetc(FILE *stream);

fgetc возвращает следующий символ из потока stream в виде unsigned char (переведенную в int) или EOF, если исчерпан файл или обнаружена ошибка.

char *fgets(char *s, int n, FILE *stream);

fgets читает не более n-1 символов в массив s, прекращая чтение, если встретился символ новой строки, который включается в массив; кроме того, записывает в массив '\0'. Функция fgets возвращает s или, если исчерпан файл или обнаружена ошибка, NULL.

int fputc(int с, FILE *stream);

fputc пишет символ c (переведенный в unsigned char) в stream. Возвращает записанный символ или EOF в случае ошибки.

int fputs(const char *s, FILE *stream);

fputs пишет строку s (которая может не иметь '\n' ) в stream; возвращает неотрицательное целое или EOF в случае ошибки.

int getc(FILE *stream);

getc делает то же, что и fgetc, но в отличие от последней, если она - макрос, stream может браться более одного раза.

int getchar(void);

getchar() делает то же, что getc(stdin).

char *gets(char *s);

gets читает следующую строку ввода в массив s, заменяя символ новой строки на '\0'. Возвращает s или, если исчерпан файл или обнаружена ошибка, NULL.

int putc(int с, FILE *stream);

putc делает то же, что и fputc, но в отличие от последней, если putc - макрос, значение stream может браться более одного раза.

int putchar(int c);

putchar(c) делает тоже, что putc(c, stdout).

int puts(const char *s);

puts пишет строку s и символ новой строки в stdout. Возвращает EOF в случае ошибки, или неотрицательное значение, если запись прошла нормально.

int ungetc(int с, FILE *stream);

ungetc отправляет символ c (переведенный в unsigned char) обратно в stream; при следующем чтении из stream он будет получен снова. Для каждого потока вернуть можно не более одного символа. Нельзя возвращать EOF. В качестве результата ungetc выдает отправленный назад символ или, в случае ошибки, EOF.

size_t fread(void *ptr, size_t size, size_t nobj, FILE *stream);

fread читает из потока stream в массив ptr не более nobj объектов размера size. Она возвращает количество прочитанных объектов, которое может быть меньше заявленного. Для индикации состояния после чтения следует использовать feof и ferror.



size_t fwrite(const void *ptr, size_t size, size_t nobj, FILE *stream);

fwrite пишет из массива ptr в stream nobj объектов размера size; возвращает число записанных объектов, которое в случае ошибки меньше nobj.

int fseek(FILE *stream, long offset, int origin);

fseek устанавливает позицию для stream; последующее чтение или запись будет производиться с этой позиции. В случае бинарного файла позиция устанавливается со смещением offset - относительно начала, если origin равен SEEK_SET; относительно текущей позиции, если origin равен SEEK_CUR; и относительно конца файла, если origin равен SEEK_END. Для текстового файла offset должен быть нулем или значением, полученным с помощью вызова функции ftell. При работе с текстовым файлом origin всегда должен быть равен SEEK_SET.

long ftell(FILE *stream);

ftell возвращает текущую позицию потока stream или -1L, в случае ошибки.

void rewind(FILE *stream);

rewind(fp) делает то же, что и fseek(fp, 0L, SEEK_SET); clearerr(fp).

int fgetpos(FILE *stream, fpos_t *ptr);

fgetpos записывает текущую позицию потока stream в *ptr для последующего использования ее в fsetpos. Тип fpos_t позволяет хранить такого рода значения, В случае ошибки fgetpos возвращает ненулевое значение.

int fsetpos(FILE *stream, const fpos_t *ptr);

fsetpos устанавливает позицию в stream, читая ее из *ptr, куда она была записана ранее с помощью fgetpos. В случае ошибки fsetpos возвращает ненулевое значение.

Многие функции библиотеки в случае ошибки или конца файла устанавливают индикаторы состояния. Эти индикаторы можно проверять и изменять. Кроме того, целое выражение errno (объявленное в <errno.h>) может содержать номер ошибки, который дает дополнительную информацию о последней из обнаруженных ошибок.

void clearerr(FILE *stream);

clearerr очищает индикаторы конца файла и ошибки потока stream.

int feof(FILE *stream);

feof возвращает ненулевое значение, если для потока stream установлен индикатор конца файла.

int ferror(FILE *stream);



ferror возвращает ненулевое значение, если для потока stream установлен индикатор ошибки.

void perror(const char *s);

perror(s) печатает s и зависимое от реализации сообщение об ошибке, соответствующее целому значению в errno, т. е. делает то же, что и обращение к функции fprintf вида

fprintf(stderr, "%s: %s\n", s, "сообщение об ошибке")

См. strerror в .

Заголовочный файл <ctype.h> объявляет функции, предназначенные для проверок символов. Аргумент каждой из них имеет тип int и должен либо представлять собой EOF, либо быть значением unsigned char, приведенным к int; возвращаемое значение тоже имеет тип int. Функции возвращают ненулевое значение ("истина"), когда аргумент c удовлетворяет описанному условию или принадлежит указанному классу символов, и нуль в противном случае.

isalnum(c) isalpha(c) или isdigit(c) есть истина
isalpha(c) isupper(c) или islower(c) есть истина
iscntrl(c) управляющий символ
isdigit(c) десятичная цифра
isgraph(c) печатаемый символ кроме пробела
islower(c) буква нижнего регистра
isprint(c) печатаемый символ, включая пробел
ispunct(c) печатаемый символ кроме пробела, буквы или цифры
isspace(c) пробел, смена страницы, новая строка, возврат каретки, табуляция, вертикальная табуляция
isupper(c) буква верхнего регистра
isxdigit(c)шестнадцатеричная цифра
В наборе семибитовых ASCII-символов печатаемые символы находятся в диапазоне от 0x20 (' ') до 0x7E ('~'); управляющие символы - от 0 (NUL) до 0x1F (US) и 0x7F (DEL).

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

int tolower(int c) – переводит c на нижний регистр; int toupper(int c) - переводит c на верхний регистр.

Если c - буква на верхнем регистре, то tolower(c) выдаст эту букву на нижнем регистре; в противном случае она вернет c. Если c - буква на нижнем регистре, то toupper(c) выдаст эту букву на верхнем регистре; в противном случае она вернет c.

Имеются две группы функций, оперирующих со строками. Они определены в заголовочном файле <string.h>. Имена функций первой группы начинаются с букв str, второй - с mem. Если копирование имеет дело с объектами, перекрывающимися по памяти, то, за исключением memmove, поведение функций не определено. Функции сравнения рассматривают аргументы как массивы элементов типа unsigned char.



В таблице на с. 321 переменные s и t принадлежат типу char *, cs и ct – типу const char *, n - типу size_t, а c - значение типа int, приведенное к типу char.

Последовательные вызовы strtok разбивают строку s на лексемы. Ограничителем лексемы служит любой символ из строки ct. В первом вызове указатель s не равен NULL. Функция находит в строке s первую лексему, состоящую из символов, не входящих в ct; ее работа заканчивается тем, что поверх следующего символа пишется '\0' и возвращается указатель на лексему. Каждый последующий вызов, в котором указатель s равен NULL, возвращает указатель на следующую лексему, которую функция будет искать сразу за концом предыдущей. Функция strtok возвращает NULL, если далее никакой лексемы не обнаружено. Параметр ct от вызова к вызову может варьироваться.

Здесь и ниже под такими выражениями как cs<ct не следует понимать арифметическое сравнение указателей. Подразумевается лексикографическое сравнение, т. е. cs меньше (больше) ct, если первый несовпавший элемент в cs арифметически меньше (больше) соответствующего элемента из ct.— Примеч. ред.

char *strcpy(s,ct) копирует строку ct в строку s, включая '\0'; возвращает s
char *strncpy(s,ct,n) копирует не более n символов строки ct в s; возвращает s. Дополняет результат символами '\0', если символов в ct меньше n
char *strcat(s,ct) приписывает ct к s; возвращает s
char *strncat(s,ct,n) приписывает не более n символов ct к s, завершая s символом '\0'; возвращает s
char strcmp(cs,st) сравнивает cs и ct; возвращает <0, если cs<ct; 0, если cs==ct; и >0, если cs>ct (I.B.: вообще-то, функция возвращает int)
char strncmp(cs,ct) сравнивает не более n символов cs и ct; возвращает <0, если cs<ct, 0, если cs==ct, и >0, если cs>ct (I.B.: тоже int должна возвращать)
char *strchr(cs,c) возвращает указатель на первое вхождение c в cs или, если такового не оказалось, NULL
char *strrchr(cs,c) возвращает указатель на последнее вхождение c в cs или, если такового не оказалось, NULL
size_t strspn(cs,ct) возвращает длину начального сегмента cs, состоящего из символов, входящих в строку ct
size_t strcspn(cs,ct) возвращает длину начального сегмента cs, состоящего из символов, не входящих в строку ct
char *strpbrk(cs,ct) возвращает указатель в cs на первый символ, который совпал с одним из символов, входящих в ct, или, если такового не оказалось, NULL
char *strstr(cs, ct) возвращает указатель на первое вхождение ct в cs или, если такового не оказалось, NULL
size_t strlen(cs) возвращает длину cs
char * strerror(n) возвращает указатель на зависящую от реализации строку, соответствующую номеру ошибки n
char * strtok(s, ct) strtok ищет в s лексему, ограниченную символами из ct; более подробное описание этой функции см. ниже
<


Функции mem... предназначены для манипулирования с объектами как с массивами символов; их назначение - получить интерфейсы к эффективным программам. В приведенной ниже таблице s и t принадлежат типу void *; cs и ct - типу const void *; n - типу size_t; а c имеет значение типа int, приведенное к типу char.

void *memcpy(s,ct, n) копирует n символов из ct в s и возвращает s
void *memmove(s,ct,n) делает то же самое, что и memcpy, но работает и в случае "перекрывающихся" объектов.
int memcmp(cs, ct, n) сравнивает первые n символов cs и ct; выдает тот же результат, что и функция strcmp
void *memchr(cs, c, n) возвращает указатель на первое вхождение символа c в cs или, если среди первых n символов c не встретилось, NULL
void *memset(s, c, n) размещает символ c в первых n позициях строки s и возвращает s
В заголовочном файле <math.h> описываются математические функции и определяются макросы.

Макросы EDOM и ERANGE (находящиеся в <errno.h>) задают отличные от нуля целочисленные константы, используемые для фиксации ошибки области и ошибки диапазона; HUGE_VAL определена как положительное значение типа double. Ошибка области возникает, если аргумент выходит за область значений, для которой определена функция. Фиксация ошибки области осуществляется присвоением errno значения EDOM; возвращаемое значение зависит от реализации. Ошибка диапазона возникает тогда, когда результат функции не может быть представлен в виде double. В случае переполнения функция возвращает HUGE_VAL с правильным знаком и в errno устанавливается значение ERANGE. Если результат оказывается меньше, чем возможно представить данным типом, функция возвращает нуль, а устанавливается ли в этом случае errno в ERANGE, зависит от реализации. Далее x и y имеют тип double, n - тип int, и все функции возвращают значения типа double. Углы в тригонометрических функциях задаются в радианах.

sin(x) синус x
cos(x) косинус x
tan(x) тангенс x
asin(x) арксинус x в диапазоне [-pi/2,pi/2], x в диапазоне [-1,1]
acos(x) арккосинус x в диапазоне [0, pi], x в диапазоне [-1,1]
atan(x) арктангенс x в диапазоне [-pi/2, pi/2]
atan2(y,x) арктангенс y/x в диапазоне [-pi, pi]
sinh(x) гиперболический синус x
cosh(x) гиперболический косинус x
tanh(x) гиперболический тангенс x
exp(x) Экспоненциальная функция ex
log(x) натуральный логарифм ln(x), x > 0
log10(x) десятичный логарифм lg(x), x > 0
pow(x,y) xy, ошибка области, если x = 0 или y<=0 или x<0 и y – не целое
sqrt(x) квадратный корень x, x >= 0
ceil(x) наименьшее целое в виде double, которое не меньше x
floor(x) наибольшее целое в виде double, которое не больше x
fabs(x) абсолютное значение |x|
ldexp(x, n) x * 2n
frexp(x, int *еxр) разбивает x на два сомножителя, первый из которых - нормализованная дробь в интервале [1/2, 1), которая возвращается, а второй - степень двойки, эта степень запоминается в *exp. Если x - нуль, то обе части результата равны нулю
modf(x,double *ip) разбивается на целую и дробную части, обе имеют тот же знак, что и x. Целая часть запоминается в *ip, дробная часть выдается как результат
fmod(x, y) остаток от деления x на y в виде числа с плавающей точкой. Знак результата совпадает со знаком x. Если y равен нулю, результат зависит от реализации
<


Заголовочный файл <stdlib.h> объявляет функции, предназначенные для преобразования чисел, запроса памяти и других задач.

double atof(const char *s)

atof переводит s в double; эквивалентна strtod(s, (char**) NULL).

int atoi(const char *s)

atoi переводит s в int; эквивалентна (int)strtol(s, (char**)NULL, 10).

int atol(const char *s)

atol переводит s в long; эквивалентна strtol(s, (char**) NULL, 10).

double strtod(const char *s, char **endp)

strtod преобразует первые символы строки s в double, игнорируя начальные символы-разделители; запоминает указатель на непреобразованный конец в *endp (если endp не NULL), при переполнении она выдает HUGE_VAL с соответствующим знаком, в случае, если результат оказывается меньше, чем возможно представить данным типом, возвращается 0; в обоих случаях в errno устанавливается ERANGE.

long strtol(const char *s, char **endp, int base)

strtol преобразует первые символы строки s в long, игнорируя начальные символы-разделители; запоминает указатель на непреобразованный конец в *endp (если endp не NULL). Если base находится в диапазоне от 2 до 36, то преобразование делается в предположении, что на входе - запись числа по основанию base. Если base равно нулю, то основанием числа считается 8, 10 или 16; число, начинающееся с цифры 0, считается восьмеричным, а с 0x или 0X - шестнадцатеричным. Цифры от 10 до base-1 записываются начальными буквами латинского алфавита в любом регистре. При основании, равном 16, в начале числа разрешается помещать 0x или 0X. В случае переполнения функция возвращает LONG_MAX или LONG_MIN (в зависимости от знака), a в errno устанавливается ERANGE.

unsigned long strtoul(const char *s, char **endp, int base)

strtoul работает так же, как и strtol, с той лишь разницей, что возвращает результат типа unsigned long, а в случае переполнения - ULONG_MAX.

int rand(void)

rand выдает псевдослучайное число в диапазоне от 0 до RAND_MAX; RAND_MAX не меньше 32767.

void srand(unsigned int seed)



srand использует seed в качестве семени для новой последовательности псевдослучайных чисел. Изначально параметр seed равен 1.

void *calloc(size_t nobj, size_t size)

calloc возвращает указатель на место в памяти, отведенное для массива nobj объектов, каждый из которых размера size, или, если памяти запрашиваемого объема нет, NULL. Выделенная область памяти обнуляется.

void *malloc(size_t size)

malloc возвращает указатель на место в памяти для объекта размера size или, если памяти запрашиваемого объема нет, NULL. Выделенная область памяти не инициализируется.

void *realloc(void *p, size_t size)

realloc заменяет на size размер объекта, на который указывает p. Для части, размер которой равен наименьшему из старого и нового размеров, содержимое не изменяется. Если новый размер больше старого, дополнительное пространство не инициализируется, realloc возвращает указатель на новое место памяти или, если требования не могут быть удовлетворены, NULL (*p при этом не изменяется).

void free(void *р)

free освобождает область памяти, на которую указывает p; эта функция ничего не делает, если p равно NULL. В p должен стоять указатель на область памяти, ранее выделенную одной из функций: calloc, malloc или realloc.

void abort(void *р)

abort вызывает аварийное завершение программы, ее действия эквивалентны вызову raise(SIGABRT).

void exit(int status)

exit вызывает нормальное завершение программы. Функции, зарегистрированные с помощью atexit, выполняются в порядке, обратном их регистрации. Производится опорожнение буферов открытых файлов, открытые потоки закрываются, и управление возвращается в среду, из которой был произведен запуск программы. Значение status, передаваемое в среду, зависит от реализации, однако при успешном завершении программы принято передавать нуль. Можно также использовать значения EXIT_SUCCESS (в случае успешного завершения) и EXIT_FAILURE (в случае ошибки).

int atexit(void (*fcn)(void))

atexit регистрирует fcn в качестве функции, которая будет вызываться при нормальном завершении программы; возвращает ненулевое значение, если регистрация не может быть выполнена.



int system(const char *s)

system пepeдaeт cтpoку s oпepaциoннoй cpeдe для выпoлнeния. Если s есть NULL и существует командный процессор, то system возвращает ненулевое значение. Если s не NULL, то возвращаемое значение зависит от реализации.

char *getenv(const char *name)

getenv возвращает строку среды, связанную с name, или, если никакой строки не существует, NULL. Детали зависят от реализации.

void *bsearch(const void *key, const void *base, size_t n, size_t size, int (*cmp)(const void *keyval, const void *datum))

bsearch ищет среди base[0]...base[n-1] элемент с подходящим ключом *key. Функция cmp должна сравнивать первый аргумент (ключ поиска) со своим вторым аргументом (значением ключа в таблице) и в зависимости от результата сравнения выдавать отрицательное число, нуль или положительное значение. Элементы массива base должны быть упорядочены в возрастающем порядке, bsearch возвращает указатель на элемент с подходящим ключом или, если такого не оказалось, NULL.

void qsort(void *base, size_t n, size_t size, int (*cmp)(const void *, const void *))

qsort сортирует массив base[0]...base[n-1] объектов размера size в возрастающем порядке. Функция сравнения cmp - такая же, как и в bsearch.

int abs(int n)

abs возвращает абсолютное значение аргумента типа int.

long labs(long n)

labs возвращает абсолютное значение аргумента типа long.

div_t div(int num, int denom)

div вычисляет частное и остаток от деления num на denom. Результаты типа int запоминаются в элементах quot и rem структуры div_t.

ldiv_t ldiv(long num, long denom)

ldiv вычисляет частное и остаток от деления num на denom. Результаты типа long запоминаются в элементах quot и rem структуры ldiv_t.

Макрос assert используется для включения в программу диагностических сообщений.

void assert (int выражение)

Если выражение имеет значение нуль, то

assert (выражение)

напечатает в stderr сообщение следующего вида:

Assertion failed: выражение, file имя-файла, line nnn

после чего будет вызвана функция abort, которая завершит вычисления. Имя исходного файла и номер строки будут взяты из макросов __FILE__ и __LINE__.



Если в момент включения файла <assert.h> было определено имя NDEBUG, то макрос assert игнорируется.

Заголовочный файл <stdarg.h> предоставляет средства для перебора аргументов функции, количество и типы которых заранее не известны. Пусть lastarg - последний именованный параметр функции f с переменным числом аргументов. Внутри f объявляется переменная ap типа va_list, предназначенная для хранения указателя на очередной аргумент:

va_list ар;

Прежде чем будет возможен доступ к безымянным аргументам, необходимо один раз инициализировать ap, обратившись к макросу va_start:

va_start(va_list ap, lastarg);

С этого момента каждое обращение к макросу:

type va_arg(va_list ap, type);

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

void va_end(va_list ap);

Объявления в <setjmp.h> предоставляют способ отклониться от обычной последовательности "вызов - возврат"; типичная ситуация - необходимость вернуться из "глубоко вложенного" вызова функции на верхний уровень, минуя промежуточные возвраты.

int setjmp(jmp_buf env);

Макрос setjmp сохраняет текущую информацию о вызовах в env для последующего ее использования в longjmp. Возвращает нуль, если возврат осуществляется непосредственно из setjmp, и не нуль, если - от последующего вызова longjmp. Обращение к setjmp возможно только в определенных контекстах, в основном это проверки в if, switсh и циклах, причем только в простых выражениях отношения.

if (setjmp() == 0) /* после прямого возврата */ else /* после возврата из longjmp */

void longjmp(jmp_buf env, int val);

longjmp восстанавливает информацию, сохраненную в самом последнем вызове setjmp, по информации из env; выполнение программы возобновляется, как если бы функция setjmp только что отработала и вернула ненулевое значение val. Результат будет непредсказуемым, если в момент обращения к longjmp функция, содержащая вызов setjmp, уже "отработала" и осуществила возврат. Доступные ей объекты имеют те значения, которые они имели в момент обращения к longjmp; setjmp не сохраняет значений.



Заголовочный файл <signal.h> предоставляет средства для обработки исключительных ситуаций, возникающих во время выполнения программы, таких как прерывание, вызванное внешним источником или ошибкой в вычислениях.

void (*signal(int sig, void (*handler)(int)))(int)

signal устанавливает, как будут обрабатываться последующие сигналы. Если параметр handler имеет значение SIG_DFL, то используется зависимая от реализации "обработка по умолчанию"; если значение handler равно SIG_IGN, то сигнал игнорируется; в остальных случаях будет выполнено обращение к функции, на которую указывает handler с типом сигнала в качестве аргумента. В число допустимых видов сигналов входят:

SIGABRT - аварийное завершение, например от abort;
SIGFPE - арифметическая ошибка: деление на 0 или переполнение;
SIGILL - неверный код функции (недопустимая команда);
SIGINT - запрос на взаимодействие, например прерывание;
SIGSEGV - неверный доступ к памяти, например выход за границы;
SIGTERM - требование завершения, посланное в программу.
signal возвращает предыдущее значение handler в случае специфицированного сигнала, или SIGERR в случае возникновения ошибки.

Когда в дальнейшем появляется сигнал sig, сначала восстанавливается готовность поведения "по умолчанию", после чего вызывается функция, заданная в параметре handler, т.е. как бы выполняется вызов (*handler)(sig). Если функция handler вернет управление назад, то вычисления возобновятся с того места, где застал программу пришедший сигнал. Начальное состояние сигналов зависит от реализации.

int raise(int sig)

raise посылает в программу сигнал sig. В случае неудачи возвращает ненулевое значение.

Заголовочный файл <time.h> объявляет типы и функции, связанные с датой и временем. Некоторые функции имеют дело с местным временем, которое может отличаться от календарного, например в связи с зонированием времени. Типы clосk_t и time_t - арифметические типы для представления времени, a struct tm содержит компоненты календарного времени:



int tm_sec; - секунды от начала минуты (0,61); -- I.B.: все же наверно от 0 до 59 int tm_min; - минуты от начала часа (0,59); int tm_hour; - часы от полуночи (0,23); int tm_mday; - число месяца (1,31); int tm_mon; - месяцы с января(0,11); int tm_year; - годы с 1900; int tm_wday; - дни с воскресенья (0,6); int tm_yday; - дни с 1 января (0,365); int tm_isdst; - признак летнего времени.

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

clock_t clock(void)

clock возвращает время, фиксируемое процессором от начала выполнения программы, или -1, если оно не известно. Для выражения этого времени в секундах применяется формула clock()/CLOCKS_PER_SEC.

time_t time(time_t *tp)

time возвращает текущее календарное время (т. е. время, прошедшее после определенной даты, - обычно после 0 ч 00 мин 00 с GMT 1-го января 1970 г. - примеч. ред.) или -1, если время не известно. Если tp не равно NULL, то возвращаемое значение записывается и в *tp.

double difftime(time_t time2, time_t time1)

difftime возвращает разность time2 - time1, выраженную в секундах.

time_t mktime(struct tm *tp)

mktime преобразует местное время, заданное структурой *tp, в календарное, выдавая его в том же виде, что и функция time. Компоненты будут иметь значения в указанных диапазонах. Функция mktime возвращает календарное время или -1, если оно не представимо.

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

char *asctime(const struct tm *tp)

asctime переводит время в структуре *tp в строку вида

Sun Jan 3 15:14:13 1988\n\0

char *ctime(const time_t *tp)

ctime переводит календарное время в местное, что эквивалентно выполнению asctime(localtime(tp))

struct tm *gmtime(const time_t *tp)

gmtime переводит календарное время во Всемирное координированное время (Coordinated Universal Time - UTC). Выдаст NULL, если UTC не известно. Имя этой функции, gmtime, происходит от Greenwich Mean Time (среднее время по Гринвичскому меридиану).



struct tm *localtime(const time_t *tp)

localtime переводит календарное время *tp в местное.

size_t strftime(char *s, size_t smax, const char *fmt, const struct tm *tp)

strftime форматирует информацию о дате и времени из *tp в строку s согласно формату fmt, который имеет много общих черт с форматом, задаваемым в функции printf. Обычные символы (включая и завершающий символ '\0') копируются в s. Каждая пара, состоящая из % и буквы, заменяется, как показано ниже, с использованием значений по форме, соответствующей местным традициям. В s размещается не более smax символов; strftime возвращает число символов без учета '\0' или нуль, если число сгенерированных символов больше smax.

%aсокращенное название дня недели
%Aполное название дня недели
%bсокращенное название месяца
%Bполное название месяца
%cместное представление даты и времени
%dдень месяца (01-31)
%Hчас (24-часовое время) (00-23)
%Iчас (12-часовое время) (01-12)
%jдень от начала года (001-366)
%mмесяц (01-12)
%Mминута (00-59)
%pместное представление AM или РМ (до или после полудня)
%Sсекунда (00-61)
%Uнеделя от начала года (считая, что воскресенье - 1-й день недели) (00-53)
%wдень недели (0-6, номер воскресенья - 0)
%Wнеделя от начала года (считая, что понедельник - 1-й день недели) (00-53)
%xместное представление даты
%Xместное представление времени
%yгод без указания века (00-99)
%Yгод с указанием века
%Zназвание временной зоны, если она есть
%%%
Заголовочный файл <limits.h> определяет константы для размеров целочисленных типов. Ниже перечислены минимальные приемлемые величины, но в конкретных реализациях могут использоваться и большие значения.

CHAR_BIT8битов в значении char
SCHAR_MAXUCHAR_MAX или SCHAR_MAXмаксимальное значение char
CHAR_MIN0 или CHAR_MINминимальное значение char
INT_MAX+32767максимальное значение int
INT_MIN-32767 (I.B.:обычно это значение -32768)минимальное значение int
LONG_MAX+2147463647максимальное значение long
LONG_MIN-2147483647 (I.B.:обычно это значение -2147483648)минимальное значение long
SCHAR_MAX+127максимальное значение signed char
SCHAR_MIN-127 (I.B.:обычно это значение -128)минимальное значение signed char
SHRT_MAX+32767максимальное значение short
SHRT_MIN-32767 (I.B.:обычно это значение -32768)минимальное значение short
UCHAR_MAX255максимальное значение unsigned char
UINT_MAX65535максимальное значение unsigned int
ULONG_MAX4294967295максимальное значение unsigned long
USHRT_MAX65535максимальное значение unsigned short
<


Имена, приведенные в следующей таблице, взяты из <float.h> и являются константами, имеющими отношение к арифметике с плавающей точкой. Значения (если они есть) представляют собой минимальные значения для соответствующих величин. В каждой реализации устанавливаются свои значения.

FLT_RADIX2основание для представления порядка, например: 2, 16
FLT_ROUNDSспособ округления при сложении чисел с плавающей точкой
FLT_DIG6количество верных десятичных цифр
FLT_EPSILON1E-5минимальное х, такое, что 1.0 + х != 1.0
FLT_MANT_DIGколичество цифр по основанию FLT_RADIX в мантиссе
FLT_MAX1E+37максимальное число с плавающей точкой
FLT_MAX_EXPмаксимальное n, такое, что FLT_RADIXn-1 представимо
FLT_MIN1E-37минимальное нормализованное число с плавающей точкой
FLT_MIN_EXPминимальное n, такое, что 10n представимо в виде нормализованного числа
DBL_DIG10количество верных десятичных цифр для типа double
DBL_EPSILON1E-9минимальное х, такое, что 1.0 + x != 1.0, где x принадлежит типу double
DBL_MANT_DIGколичество цифр по основанию FLT_RADIX в мантиссе для чисел типа double
DBL_MAX1E+37максимальное число с плавающей точкой типа double
DBL_MAX_EXPмаксимальное n, такое, что FLT_RADIXn-1 представимо в виде числа типа double
DBL_MIN1E-37минимальное нормализованное число с плавающей точкой типа double
DBL_MIN_EXPминимальное n, такое, что 10n представимо в виде нормализованного числа типа double



C. Перечень изменений


С момента публикации первого издания этой книги определение языка Си претерпело изменения. Почти все нововведения - это расширения исходной версии языка, выполненные так, чтобы сохранилась совместимость с существующими программами; некоторые изменения касаются устранения двусмысленностей первоначального описания, а некоторые представляют собой модификации, привнесенные существующей практикой. Многие из новых возможностей, впоследствии принятые другими разработчиками Си-компиляторов, были первоначально объявлены в документах, прилагаемых к компиляторам. Комитет ANSI, подготавливая стандарт языка, включил большинство этих изменений, а также ввел другие значительные модификации. Некоторые коммерческие компиляторы реализовали их еще до выпуска официального Си-стандарта.

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

В стандарте более тщательно, по сравнению с первым изданием, определено и расширено препроцессирование: в его основу явно положены лексемы; введены новые операторы для "склеивания" лексем (##) и создания символьных строк (#), а также новые управляющие строки, такие как #elif и #pragma; разрешено повторное определение макроса с той же последовательностью лексем; отменена подстановка параметров внутри строк. Разрешено "склеивание" строк с помощью знака \ в любом месте, не только в строках и макроопределениях (см. ).

Минимальное число значимых символов всех внутренних идентификаторов доведено до 31; для идентификаторов с внешней связью оно остается равным 6; буквы нижнего и верхнего регистров не различаются. (Многие реализации допускают большее число значимых символов.)


Для знаков #, \, ^, [, ], {, }, |, ~, которых может не быть в некоторых наборах символов, введены трехзнаковые последовательности, начинающиеся с ?? (см. ). Следует заметить, что введение трехзнаковых последовательностей может повредить значения строк, в которых содержатся ??.

Введены новые ключевые слова (void, const, volatile, signed, enum), а мертворожденное слово entry из обращения изъято.

Для символьных констант и строковых литералов определены новые эскейп- последовательности. Объявлено, что появление за \ символов не из принятых эскейп-последовательностей приводит к непредсказуемому результату (см. .).

Узаконено полюбившееся всем тривиальное изменение: 8 и 9 не являются восьмеричными цифрами.

Введен расширенный набор суффиксов для явного указания типов констант: U и L - для целых и F и L - для типов с плавающей точкой. Уточнены также правила определения типа для констант без суффиксов ().

Объявлено, что соседние строки конкатенируются.

Предоставлены средства, позволяющие записывать строковые литералы и символьные константы из расширенного набора символов ().

Объекты типа char (как и объекты другого типа) можно специфицировать явно со знаком или без знака. Исключается использование словосочетания long float в смысле double, но вводится тип long double для чисел с плавающей точкой повышенной точности.

С некоторых пор доступен тип unsigned char. Стандарт вводит ключевое слово signed для явного указания, что объект типа char или другого целочисленного типа имеет знак.

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

Стандарт устанавливает минимальные пределы диапазонов арифметических типов, предусматривает заголовочные файлы <limits.h> и <float.h>, в которых помещаются эти характеристики для каждой конкретной реализации.



Перечисление - новый тип, которого не было в первой редакции.

Стандарт заимствует из C++ способ записи квалификатора типа, в частности квалификатора const ().

Вводится запрет на модификацию строк: это значит, что их разрешается размещать в памяти, доступной только на чтение (ПЗУ).

Изменены "обычные арифметические преобразования"; по существу, выполнен переход от принципа "для целых всегда превалирует unsigned; для плавающей точки всегда используется double" к принципу "повышение до минимального достаточно вместительного типа" (см. ).

Отменены старые операторы присваивания вроде =+. Каждый оператор присваивания теперь представляется одной отдельной лексемой. В первом издании оператор присваивания мог изображаться парой символов, возможно, разделенных символами-разделителями.

Компиляторам более не разрешается трактовать математическую ассоциативность операторов как вычислительную ассоциативность.

Введен унарный оператор + для симметрии с унарным -.

Разрешено использовать указатель на функцию в качестве ее именующего выражения без явного оператора * (см. ).

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

Разрешено применять оператор получения адреса & к массиву; результатом является указатель на массив.

В первой редакции результат операции sizeof имел тип int; во многих реализациях он заменен на unsigned. Стандарт официально объявляет его зависимым от реализации, но требует, чтобы он был определен в заголовочном файле <stddef.h> под именем size_t. Аналогичное изменение было сделано в отношении типа разности указателей) который теперь выступает под именем ptrdiff_t (см. и ).

Запрещено применять оператор получения адреса & к объекту типа register даже тогда, когда данный компилятор не располагает его на регистре.

Типом результата операции сдвига является тип ее левого операнда; тип правого операнда на повышение типа результата влияния не оказывает (см. ).



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

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

Стандартом запрещены пустые объявления, т. е. такие, в которых нет объявителей и не объявляется ни одной структуры, объединения или перечисления. Однако объявление с одним тегом структуры (или объединения) переобъявит ее даже в том случае, если она была объявлена во внешней области действия.

Запрещены объявления внешних данных, не имеющие спецификаторов и квалификаторов (т. е. объявления с одним "голым" объявителем).

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

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

Несколько изменены именные пространства идентификаторов. Всем тегам структур, объединений и перечислений стандарт выделяет одно именное пространство; для меток инструкций вводится отдельное именное пространство (см. ). Кроме того, имена элементов связаны со структурой или объединением, частью которого они являются. (С некоторых пор это общепринятая практика.)

Допускается инициализация объединения; инициализатор относится к первому элементу объединения.

Разрешается инициализация автоматических структур, объединений и массивов, хотя и с некоторыми ограничениями.

Разрешается инициализация массива символов с помощью строкового литерала по точному количеству указанных символов (без '\0').

Управляющее выражение и case-метки в switch могут иметь любой целочисленный тип.


Приоритет и очередность вычислений


В таблице 2.1 показаны приоритеты и очередность вычислений всех операторов, включая и те, которые мы еще не рассматривали. Операторы, перечисленные на одной строке, имеют одинаковый приоритет: строки упорядочены по убыванию приоритетов; так, например, *, / и % имеют одинаковый приоритет, который выше, чем приоритет бинарных + и -. "Оператор” () относится к вызову функции. Операторы -> и . (точка) обеспечивают доступ к элементам структур; о них пойдет речь в , там же будет рассмотрен и оператор sizeof (размер объекта). Операторы * (косвенное обращение по указателю) и & (получение адреса объекта) обсуждаются в . Оператор "запятая” будет рассмотрен в .

Таблица 2.1. Приоритеты и очередность вычислений операторов

Операторы

Выполняются

() [] -> . слева направо
! ~ ++ -- + - * & (type) sizeof справа налево
* / % слева направо
+ - слева направо
>> слева направо
> >= слева направо
== != слева направо
& слева направо
^ слева направо
| слева направо
&& слева направо
слева направо
?: справа налево
= += -= *= /= %= &= ^= |= >>= справа налево
, слева направо

Примечание. Унарные операторы +, -, * и & имеют более высокий приоритет, чем те же бинарные операторы.

Заметим, что приоритеты побитовых операторов &, ^ и | ниже, чем приоритет == и != , из-за чего в побитовых проверках, таких как

if ((x & MASK) == 0) ...

чтобы получить правильный результат, приходится использовать скобки. Си подобно многим языкам не фиксирует очередность вычисления операндов оператора (за исключением &&, , ?: и ,). Например, в инструкции вида

x = f() + g();

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


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

printf("%d %d\n", ++n, power(2, n)); /* НЕВЕРНО*/

может давать несовпадающие результаты. Результат вызова функции зависит от того, когда компилятор сгенерирует команды увеличения n - до или после обращения к power. Чтобы обезопасить себя от возможного побочного эффекта, достаточно написать

++n; printf("%d %d\n", n, power(2, n));

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

a[i] = i++; /* I.B.: doubtful example */

возникает вопрос: массив a индексируется старым или измененным значением i? Компиляторы могут по-разному генерировать программу, что проявится в интерпретации данной записи. Стандарт сознательно устроен так, что большинство подобных вопросов оставлено на усмотрение компиляторов, так как лучший порядок вычислений определяется архитектурой машины. Стандартом только гарантируется, что все побочные эффекты при вычислении аргументов проявятся перед входом в функцию. Правда, в примере с printf это нам не поможет.

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


Просмотр таблиц


В этом параграфе, чтобы проиллюстрировать новые аспекты применения структур, мы напишем ядро пакета программ, осуществляющих вставку элементов в таблицы и их поиск внутри таблиц. Этот пакет - типичный набор программ, с помощью которых работают с таблицами имен в любом макропроцессоре или компиляторе. Рассмотрим, например, инструкцию #define. Когда встречается строка вида

#define IN 1

имя IN и замещающий его текст 1 должны запоминаться в таблице. Если затем имя IN встретится в инструкции, например в

state = IN;

это должно быть заменено на 1.

Существуют две программы, манипулирующие с именами и замещающими их текстами. Это install(s,t), которая записывает имя s и замещающий его текст t в таблицу, где s и t - строки, и lookup(s), осуществляющая поиск s в таблице и возвращающая указатель на место, где имя s было найдено, или NULL, если s в таблице не оказалось.

Алгоритм основан на хэш-поиске: поступающее имя свертывается в неотрицательное число (хэш-код), которое затем используется в качестве индекса в массиве указателей. Каждый элемент этого массива является указателем на начало связанного списка блоков, описывающих имена с данным хэш-кодом. Если элемент массива равен NULL, это значит, что имен с соответствующим хэш-кодом нет.

Блок в списке - это структура, содержащая указатели на имя, на замещающий текст и на следующий блок в списке; значение NULL в указателе на следующий блок означает конец списка.

struct nlist { /* элемент таблицы */ struct nlist *next; /* указатель на следующий элемент */ char *name; /* определенное имя */ char *defn; /* замещающий текст */ };

А вот как записывается определение массива указателей:

#define HASHSIZE 101 static struct nlist *hashtab[HASHSIZE]; /* таблица указателей */

Функция хэширования, используемая в lookup и install, суммирует коды символов в строке и в качестве результата выдаст остаток от деления полученной суммы на размер массива указателей. Это не самая лучшая функция хэширования, но достаточно лаконичная и эффективная.

/* hash: получает хэш-код для строки s */ unsigned hash(char *s) { unsigned hashval;


for (hashval = 0; *s != '\0'; s++) hashval = *s + 31 * hashval; return hashval % HASHSIZE; }

Беззнаковая арифметика гарантирует, что хэш-код будет неотрицательным.

Хэширование порождает стартовый индекс для массива hashtab; если соответствующая строка в таблице есть, она может быть обнаружена только в списке блоков, на начало которого указывает элемент массива hashtab с этим индексом. Поиск осуществляется с помощью lookup. Если lookup находит элемент с заданной строкой, то возвращает указатель на нее, если не находит, то возвращает NULL.

/* lookup: ищет s */ struct nlist *lookup(char *s) { struct nlist *np;

for (np = hashtab[hash(s)]; np != NULL; np = np->next) if (strcmp(s, np->name) == 0) return np; /* нашли */ return NULL; /* не нашли */ }

В for-цикле функции lookup для просмотра списка используется стандартная конструкция

for (ptr = head; ptr != NULL; ptr = ptr->next) ...

Функция install обращается к lookup, чтобы определить, имеется ли уже вставляемое имя. Если это так, то старое определение будет заменено новым. В противном случае будет образован новый элемент. Если запрос памяти для нового элемента не может быть удовлетворен, функция install выдает NULL.

struct nlist *lookup(char *); char *strdup(char *);

/* install: заносит имя и текст (name, defn) в таблицу */ struct nlist *install(char *name, char *defn) { struct nlist *np; unsigned hashval;

if ((np = lookup(name)) == NULL) { /* не найден */ np = (struct nlist *) malloc(sizeof(*np)); if (np == NULL (np->name = strdup(name)) == NULL) return NULL; hashval = hash(name); np->next = hashtab[hashval]; hashtab[hashval] = np; } else /* уже имеется */ free((void *) np->defn); /* освобождаем прежний defn */ if ((np->defn = strdup(defn)) == NULL) return NULL; return np; }

Упражнение 6.5. Напишите функцию undef, удаляющую имя и определение из таблицы, организация которой поддерживается функциями lookup и install.

Упражнение 6.6. Реализуйте простую версию #define-npoцeccopa (без аргументов), которая использовала бы программы этого параграфа и годилась бы для Си-программ. Вам могут помочь программы getch и ungetch.


Регистровые переменные


Объявление register сообщает компилятору, что данная переменная будет интенсивно использоваться. Идея состоит в том, чтобы переменные, объявленные register, разместить на регистрах машины, благодаря чему программа, возможно, станет более короткой и быстрой. Однако компилятор имеет право проигнорировать это указание. Объявление register выглядит следующим образом:

register int х; register char с;

и т. д. Объявление register может применяться только к автоматическим переменным и к формальным параметрам функции. Для последних это выглядит так:

f(register unsigned m, register long n) { register int i; ... }

На практике существуют ограничения на регистровые переменные, что связано с возможностями аппаратуры. Располагаться в регистрах может лишь небольшое число переменных каждой функции, причем только определенных типов. Избыточные объявления register ни на что не влияют, так как игнорируются в отношении переменных, которым не хватило регистров или которые нельзя разместить на регистре. Кроме того, применительно к регистровой переменной независимо от того, выделен на самом деле для нее регистр или нет, не определено понятие адреса (см. ). Конкретные ограничения на количество и типы регистровых переменных зависят от машины.



Рекурсия


В Си допускается рекурсивное обращение к функциям, т. е. функция может обращаться сама к себе, прямо или косвенно. Рассмотрим печать числа в виде строки символов. Как мы упоминали ранее, цифры генерируются в обратном порядке - младшие цифры получаются раньше старших, а печататься они должны в правильной последовательности.

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

#include <stdio.h>

/* printd: печатает n как целое десятичное число */ void printd(int n) { if (n < 0) { putchar('-'); n = -n; } if (n / 10) printd(n / 10); putchar(n % 10 + '0'); }

Когда функция рекурсивно обращается сама к себе, каждое следующее обращение сопровождается получением ею нового полного набора автоматических переменных, независимых от предыдущих наборов. Так, в обращении printd(123) при первом вызове аргумент n = 123, при втором - printd получает аргумент 12, при третьем вызове - значение 1. Функция printd на третьем уровне вызова печатает 1 и возвращается на второй уровень, после чего печатает цифру 2 и возвращается на первый уровень. Здесь она печатает 3 и заканчивает работу.

Следующий хороший пример рекурсии - это быстрая сортировка, предложенная Ч.А.Р. Хоаром в 1962 г. Для заданного массива выбирается один элемент, который разбивает остальные элементы на два подмножества - те, что меньше, и те, что не меньше него. Та же процедура рекурсивно применяется и к двум полученным подмножествам. Если в подмножестве менее двух элементов, то сортировать нечего, и рекурсия завершается.

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


/* qsort: сортирует v[left]...v[right] по возрастанию */ void qsort(int v[], int left, int right) { int i, last; void swap(int v[], int i, int j);

if (left >= right) /* ничего не делается, если */ return; /* в массиве менее двух элементов */ swap(v, left, (left + right)/2); /* делящий элемент */ last = left; /* переносится в v[0] */ for(i = left+1; i

В нашей программе операция перестановки оформлена в виде отдельной функции (swap), поскольку встречается в qsort трижды.

/* swap: поменять местами v[i] и v[j] */ void swap(int v[], int i, int j) { int temp; temp = v[i]; v[i] = v[j]; v[j] = temp; }

Стандартная библиотека имеет функцию qsort, позволяющую сортировать объекты любого типа.

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

Упражнение 4.12. Примените идеи, которые мы использовали в printd, для написания рекурсивной версии функции itoa; иначе говоря, преобразуйте целое число в строку цифр с помощью рекурсивной программы.

Упражнение 4.13. Напишите рекурсивную версию функции reverse(s), переставляющую элементы строки в ту же строку в обратном порядке.


Символьные массивы


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

while (есть ли еще строка?) if (данная строка длиннее самой длинной из предыдущих) запомнить ее

запомнить ее длину

напечатать самую длинную строку

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

Поскольку процесс четко распадается на части, хорошо бы так и перевести его на Си. Поэтому сначала напишем отдельную функцию getline для получения очередной строки. Мы попытаемся сделать эту функцию полезной и для других применений. Как минимум getline должна сигнализировать о возможном конце файла, а еще лучше, если она будет выдавать длину строки — или нуль в случае исчерпания файла. Нуль годится для признака конца файла, поскольку не бывает строк нулевой длины, даже строка, содержащая только один символ новой строки, имеет длину 1.

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

Наконец, нам необходима главная программа, которая бы управляла функциями getline и copy. Вот как выглядит наша программа в целом:

#include <stdio.h> #define MAXLINE 1000 /* максимальный размер вводимой строки */

int getline(char line[], int MAXLINE); void copy(char to[], char from[]);

/* печать самой длинной строки */ main() { int len; /* длина текущей строки */ int max; /* длина максимальной из просмотренных строк */ char line[MAXLINE]; /* текущая строка */ char longest[MAXLINE]; /* самая длинная строка */

max = 0; while (len = getline(line, MAXLINE)) > 0) if (len > max) { max = len; copy(longest, line); } if (max > 0) /* была ли хоть одна строка? */ printf(“%s”, longest); return 0; }


/* getline: читает строку в s, возвращает длину */ int getline(char s[], int lim) { int c, i;

for (i = 0; i < lim-1 && (c = getchar()) != EOF && с != '\n'; ++i) s[i] = c; if (c == '\n') { s[i] = c; ++i; } s[i] = '\0'; return i; }

/* copy: копирует из 'from' в 'to'; to достаточно большой */ void copy(char to[], char from[]) { int i;

i = 0; while ((to[i] = from[i]) != '\0') ++i; }

Мы предполагаем, что функции getline и copy, описанные в начале программы, находятся в том же файле, что и main.

Функции main и getline взаимодействуют между собой через пару аргументов и возвращаемое значение. В getline аргументы определяются строкой

int getline(char s[], int lim);

Как мы видим, ее первый аргумент s есть массив, а второй, lim, имеет тип int. Задание размера массива в определении имеет целью резервирование памяти. В самой getline задавать длину массива s нет необходимости, так как его размер указан в main. Чтобы вернуть значение вызывающей программе, getline использует return точно так же, как это делает функция power. В приведенной строке также сообщается, что getline возвращает значение типа int, но так как при отсутствии указания о типе подразумевается int, то перед getline слово int можно опустить.

Одни функции возвращают результирующее значение, другие (такие как copy) нужны только для того, чтобы произвести какие-то действия, не выдавая никакого значения. На месте типа результата в copy стоит void. Это явное указание на то, что никакого значения данная функция не возвращает.

Функция getline в конец создаваемого ею массива помещает символ '\0' (null-символ, кодируемый нулевым байтом), чтобы пометить конец строки символов. То же соглашение относительно окончания нулем соблюдается и в случае строковой константы вроде

“hello\n”

В данном случае для него формируется массив из символов этой строки с '\0' в конце.



h е l l o \n \0

Спецификация %s в формате printf предполагает, что соответствующий ей аргумент - строка символов, оформленная указанным выше образом. Функция copy в своей работе также опирается на тот факт, что читаемый ею аргумент заканчивается символом '\0', который она копирует наряду с остальными символами. (Всё сказанное предполагает, что '\0' не встречается внутри обычного текста.)



Попутно стоит заметить, что при работе даже с такой маленькой программой мы сталкиваемся с некоторыми конструктивными трудностями. Например, что должна делать main, если встретится строка, превышающая допустимый размер? Функция getline работает надежно: если массив полон, она прекращает пересылку, даже если символа новой строки не обнаружила. Получив от getline длину строки и увидев, что она совпадает с MAXLINE, главная программа main могла бы “отловить” этот особый случай и справиться с ним. В интересах краткости описание этого случая мы здесь опускаем.

Пользователи getline не могут заранее узнать, сколь длинными будут вводимые строки, поэтому getline делает проверки на переполнение. А вот пользователям функции copy размеры копируемых строк известны (или они могут их узнать), поэтому дополнительный контроль здесь не нужен.

Упражнение 1.16. Перепишите main предыдущей программы так, чтобы она могла печатать самую длинную строку без каких-либо ограничений на ее размер.

Упражнение 1.17. Напишите программу печати всех вводимых строк, содержащих более 80 символов.

Упражнение 1.18. Напишите программу, которая будет в каждой вводимой строке заменять стоящие подряд символы пробелов и табуляций на один пробел и удалять пустые строки.

Упражнение 1.19. Напишите функцию reverse(s), размещающую символы в строке s в обратном порядке. Примените ее при написании программы, которая каждую вводимую строку располагает в обратном порядке.


Символьные указатели функции


Строковая константа, написанная в виде

"Я строка"

есть массив символов. Во внутреннем представлении этот массив заканчивается нулевым символом '\0', по которому программа может найти конец строки. Число занятых ячеек памяти на одну больше, чем количество символов, помещенных между двойными кавычками.

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

printf("здравствуй, мир\n");

Когда такая символьная строка появляется в программе, доступ к ней осуществляется через символьный указатель; printf получает указатель на начало массива символов. Точнее, доступ к строковой константе осуществляется через указатель на ее первый элемент.

Строковые константы нужны не только в качестве аргументов функций. Если, например, переменную pmessage объявить как

char *pmessage;

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

pmessage = "now is the time";

поместит в нее указатель на символьный массив, при этом сама строка не копируется, копируется лишь указатель на нее. Операции для работы со строкой как с единым целым в Си не предусмотрены.

Существует важное различие между следующими определениями:

char amessage[] = "now is the time"; /* массив */ char *pmessage = "now is the time"; /* указатель */

amessage - это массив, имеющий такой объем, что в нем как раз помещается указанная последовательность символов и '\0'. Отдельные символы внутри массива могут изменяться, но amessage всегда указывает на одно и то же место памяти. В противоположность ему pmessage есть указатель, инициализированный так, чтобы указывать на строковую константу. А значение указателя можно изменить, и тогда последний будет указывать на что-либо другое. Кроме того, результат будет неопределен, если вы попытаетесь изменить содержимое константы.

Дополнительные моменты, связанные с указателями и массивами, проиллюстрируем на несколько видоизмененных вариантах двух полезных программ, взятых нами из стандартной библиотеки. Первая из них, функция strcpy (s, t), копирует строку t в строку s. Хотелось бы написать прямо s = t, но такой оператор копирует указатель, а не символы. Чтобы копировать символы, нам нужно организовать цикл. Первый вариант strcpy, с использованием массива, имеет следующий вид:

/* strcpy: копирует t в s; вариант с индексируемым массивом*/ void strcpy(char *s, char *t) { int i; i = 0; while ((s[i] = t[i]) != '\0') i++; }


Для сравнения приведем версию strcpy с указателями:

/* strcpy: копирует t в s: версия 1 (с указателями) */ void strcpy(char *s, char *t) { while ((*s = *t) != '\0') { s++; t++; } }

Поскольку передаются лишь копии значений аргументов, strcpy может свободно пользоваться параметрами s и t как своими локальными переменными. Они должным образом инициализированы указателями, которые продвигаются каждый раз на следующий символ в каждом из массивов до тех пор, пока в копируемой строке t не встретится '\0'.

На практике strcpy так не пишут. Опытный программист предпочтет более короткую запись:

/* strcpy: копирует t в s; версия 2 (с указателями) */ void strcpy(char *s, char *t) { while ((*s++ = *t++) != '\0') ; }

Приращение s и t здесь осуществляется в управляющей части цикла. Значением *t++ является символ, на который указывает переменная t перед тем, как ее значение будет увеличено; постфиксный оператор ++ не изменяет указатель t, пока не будет взят символ, на который он указывает. То же в отношении s: сначала символ запомнится в позиции, на которую указывает старое значение s, и лишь после этого значение переменной s увеличится. Пересылаемый символ является одновременно и значением, которое сравнивается с '\0'. В итоге копируются все символы, включая и заключительный символ '\0'.

Заметив, что сравнение с '\0' здесь лишнее (поскольку в Си ненулевое значение выражения в условии трактуется и как его истинность), мы можем сделать еще одно и последнее сокращение текста программы:

/* strcpy: копирует t в s; версия 3 (с указателями) */ void strcpy(char *s, char *t) { while (*s++ = *t++) ; }

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

Что касается функции strcpy из стандартной библиотеки <string.h> то она возвращает в качестве своего результата еще и указатель на новую копию строки.

Вторая программа, которую мы здесь рассмотрим, это strcmp(s,t). Она сравнивает символы строк s и t и возвращает отрицательное, нулевое или положительное значение, если строка s соответственно лексикографически меньше, равна или больше, чем строка t. Результат получается вычитанием первых несовпадающих символов из s и t.



/* strcmp: выдает < 0 при s < t, 0 при s == t, > 0 при s > t */ int strcmp(char *s, char *t) { int i; for (i = 0; s[i]== t[i]; i++) if (s[i] == '\0') return 0; return s[i] - t[i]; }

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

/* strcmp: выдает < 0 при s < t, 0 при s == t, > 0 при s > t */ int strcmp(char *s, char *t) { for ( ; *s == *t; s++, t++) if (*s == '\0') return 0; return *s - *t; }

Поскольку операторы ++ и -- могут быть или префиксными, или постфиксными, встречаются (хотя и не так часто) другие их сочетания с оператором *. Например.

*--p;

уменьшит p прежде, чем по этому указателю будет получен символ. Например, следующие два выражения:

*p++ = val; /* поместить val в стек */ val = *--p; /* взять из стека значение и поместить в val */

являются стандартными для посылки в стек и взятия из стека (см. .).

Объявления функций, упомянутых в этом параграфе, а также ряда других стандартных функций, работающих со строками, содержатся в заголовочном файле <string.h>.

Упражнение 5.3. Используя указатели, напишите функцию strcat, которую мы рассматривали в (функция strcat(s,t) копирует строку t в конец строки s).

Упражнение 5.4. Напишите функцию strend(s,t), которая выдает 1, если строка t расположена в конце строки s, и нуль в противном случае.

Упражнение 5.5. Напишите варианты библиотечных функций strncpy, strncat и strncmp, которые оперируют с первыми символами своих аргументов, число которых не превышает n. Например, strncpy(t,s,n) копирует не более n символов t в s. Полные описания этих функций содержатся в .

Упражнение 5.6. Отберите подходящие программы из предыдущих глав и упражнений и перепишите их, используя вместо индексирования указатели. Подойдут, в частности, программы getline ( и ), atoi, itoa и их варианты (, и ), reverse (глава 3), а также strindex и getop ().


Сложные объявления


Иногда Си ругают за синтаксис объявлений, особенно тех, которые содержат в себе указатели на функции. Таким синтаксис получился в результате нашей попытки сделать похожими объявления объектов и их использование. В простых случаях этот синтаксис хорош, однако в сложных ситуациях он вызывает затруднения, поскольку объявления перенасыщены скобками и их невозможно читать слева направо. Проблему иллюстрирует различие следующих двух объявлений:

int *f(); /* f: функция, возвращающая ук-ль на int */ int (*pf)(); /* pf: ук-ль на ф-цию, возвращающую int */

Приоритет префиксного оператора * ниже, чем приоритет (), поэтому во втором случае скобки необходимы.

Хотя на практике по-настоящему сложные объявления встречаются редко, все же важно знать, как их понимать, а если потребуется, и как их конструировать. Укажем хороший способ: объявления можно синтезировать, двигаясь небольшими шагами с помощью typedef, этот способ рассмотрен в . В настоящем параграфе на примере двух программ, осуществляющих преобразования правильных Си-объявлений в соответствующие им словесные описания и обратно, мы демонстрируем иной способ конструирования объявлений. Словесное описание читается слева направо.

Первая программа, dcl, - более сложная. Она преобразует Си-объявления в словесные описания так, как показано в следующих примерах:

char **argv argv: указ. на указ. на char int (*daytab)[13] daytab: указ. на массив[13] из int int (*daytab)[13] daytab: массив[13] из указ. на int void *comp() comp: функц. возвр. указ. на void void (*comp)() comp: указ. на функц. возвр. void char (*(*x())[])() x: функц. возвр. указ. на массив[] из указ. на функц. возвр. char char(*(*x[3])())[5] x: массив[3] из указ. на функц. возвр. указ. на массив[5] из char

Функция dcl в своей работе использует грамматику, специфицирующую объявитель. Эта грамматика строго изложена в приложения A, а в упрощенном виде записывается так:

объявитель: необязательные * собственно-объявитель

собственно-объявитель: имя

(объявитель) собственно-объявитель() собственно-объявитель [необязательный размер]


Говоря простым языком, объявитель есть собственно-объявитель, перед которым может стоять * (т. е. одна или несколько звездочек), где собственно- объявитель есть имя, или объявитель в скобках, или собственно-объявитель с последующей парой скобок, или собственно-объявитель с последующей парой квадратных скобок, внутри которых может быть помещен размер.

Эту грамматику можно использовать для грамматического разбора объявлений. Рассмотрим, например, такой объявитель:

(*pfa[])()

Имя pfa будет классифицировано как имя и, следовательно, как собственно- объявитель. Затем pfa[] будет распознано как собственно-объявитель, а *pfa[] - как объявитель и, следовательно, (*pfa[]) есть собственно-объявитель. Далее, (*pfa[])() есть собственно-объявитель и, таким образом, объявитель. Этот грамматический разбор можно проиллюстрировать деревом разбора, приведенным на следующей странице (где собственно-объявитель обозначен более коротко, а именно собств.-объяв.).

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



/* dcl: разбор объявителя */ void dcl(void) { int ns;

for (ns = 0, gettoken() == '*';) /* подсчет звездочек */ ns++;

dirdcl(); while(ns-- > 0) strcat(out, "указ. на”); }

/* dirdcl: разбор собственно объявителя */ void dirdcl(void) { int type;

if (tokentype == '(') { dcl(); if (tokentype != ')') printf("oшибкa: пропущена )\n"); } else if (tokentype == NAME) /* имя переменной */ strcpy(name, token); else printf("ошибка: должно быть name или (dcl)\n");

while((type = gettoken()) == PARENS type == BRACKETS) if (type == PARENS) strcat(out, "функц. возвр."); else { strcat(out, " массив"); strcat(out, token); strcat(out, " из"); } }



Приведенные программы служат только иллюстративным целям и не вполне надежны. Что касается dcl, то ее возможности существенно ограничены. Она может работать только с простыми типами вроде char и int и не справляется с типами аргументов в функциях и с квалификаторами вроде const. Лишние пробелы для нее опасны. Она не предпринимает никаких мер по выходу из ошибочной ситуации, и поэтому неправильные описания также ей противопоказаны. Устранение этих недостатков мы оставляем для упражнений. Ниже приведены глобальные переменные и главная программа main.

#include <stdio.h> #include <string.h> #include <ctype.h>

#define MAXTOKEN 100

enum {NAME, PARENS, BRACKETS};

void dcl(void); void dirdcl(void);

int gettoken(void); int tokentype; /* тип последней лексемы */ char token[MAXTOKEN]; /* текст последней лексемы */ char name[MAXTOKEN]; /* имя */ char datatype[MAXTOKEN]; /* тип = char, int и т.д. */ char out[1000]; /* выдаваемый текст */

main() /* преобразование объявления в словесное описание */ { while (gettoken() != EOF) { /* 1-я лексема в строке */ strcpy(datatype, token); /* это тип данных */ out[0] = '\0'; dcl(); /* разбор остальной части строки */ if (tokentype != '\n') printf("синтаксическая ошибка\n"); printf("%s: %s %s\n", name, out, datatype); } return 0; }

Функция gettoken пропускает пробелы и табуляции и затем получает следующую лексему из ввода: "лексема" (token) - это имя, или пара круглых скобок, или пара квадратных скобок (быть может, с помещенным в них числом), или любой другой единичный символ.

int gettoken(void) /* возвращает следующую лексему */ { int с, getch(void); void ungetch(int); char *p = token;

while ((c = getch()) == ' ' с = '\t') ; if (c == '(') { if ((c = getch())== ')' { strcpy(token, "()"); return tokentype = PARENS; } else { ungetch(c); return tokentype = '('; } } else if (c == '['){ for (*p++ = c; (*p++ = getch()) != ']'; ) ; *p = '\0'; return tokentype = BRACKETS; } else if (isalpha(c)) { for (*p++ = c; isalnum(c = getch()); ) *p++ = c; *p = '\0'; ungetch(c); return tokentype = NAME; } else return tokentype = c; }



Функции getch и ungetch были рассмотрены в .

Обратное преобразование реализуется легче, особенно если не придавать значения тому, что будут генерироваться лишние скобки. Программа undcl превращает фразу вроде "x есть функция, возвращающая указатель на массив указателей на функции, возвращающие char", которую мы будем представлять в виде

х () * [] * () char

в объявление

char (*(*х())[])()

Такой сокращенный входной синтаксис позволяет повторно пользоваться функцией gettoken. Функция undcl использует те же самые внешние переменные, что и dcl.

/* undcl: преобразует словесное описание в объявление */ main() { int type; char temp[MAXTOKEN];

while (gettoken() != EOF) { strcpy(out, token); while ((type = gettoken()) != '\n') if (type == PARENS type == BRACKETS) strcat(out, token); else if (type == '*' ) { sprintf(temp, "(*%s)", out); strcpy(out, temp); } else if (type == NAME) { sprintf(temp, "%s %s", token, out); strcpy(out, temp); } else printf( "неверный элемент %s в фразе\n", token); printf("%s\n", out); } return 0; }

Упражнение 5.18. Видоизмените dcl таким образом, чтобы она обрабатывала ошибки во входной информации.

Упражнение 5.19. Модифицируйте undcl так, чтобы она не генерировала лишних скобок.

Упражнение 5.20. Расширьте возможности dcl, чтобы dcl обрабатывала объявления с типами аргументов функции, квалификаторами вроде const и т. п.


Списки аргументов переменной длины


Этот параграф содержит реализацию минимальной версии printf. Приводится она для того, чтобы показать, как надо писать функции со списками аргументов переменной длины, причем такие, которые были бы переносимы. Поскольку нас главным образом интересует обработка аргументов, функцию minprintf напишем таким образом, что она в основном будет работать с задающей формат строкой и аргументами; что же касается форматных преобразований, то они будут осуществляться с помощью стандартного printf.

Объявление стандартной функции printf выглядит так:

int printf(char *fmt, ...)

Многоточие в объявлении означает, что число и типы аргументов могут изменяться. Знак многоточие может стоять только в конце списка аргументов. Наша функция minprintf объявляется как

void minprintf(char *fmt, ...)

поскольку она не будет выдавать число символов, как это делает printf.

Вся сложность в том, каким образом minprintf будет продвигаться вдоль списка аргументов, - ведь у этого списка нет даже имени. Стандартный заголовочный файл <stdarg.h> содержит набор макроопределений, которые устанавливают, как шагать по списку аргументов. Наполнение этого заголовочного файла может изменяться от машины к машине, но представленный им интерфейс везде одинаков.

Тип va_list служит для описания переменной, которая будет по очереди указывать на каждый из аргументов; в minprintf эта переменная имеет имя ap (от "argument pointer" - указатель на аргумент). Макрос va_start инициализирует переменную ap) чтобы она указывала на первый безымянный аргумент. К va_start нужно обратиться до первого использования ap. Среди аргументов по крайней мере один должен быть именованным: от последнего именованного аргумента этот макрос "отталкивается" при начальной установке.

Макрос va_arg на каждом своем вызове выдает очередной аргумент, а ap передвигает на следующий: но имени типа он определяет тип возвращаемого значения и размер шага для выхода на следующий аргумент. Наконец, макрос va_end делает очистку всего, что необходимо. К va_end следует обратиться перед самым выходом из функции.


Перечисленные средства образуют основу нашей упрощенной версии prinf.

#include <stdarg.h>

/* minprintf: минимальный printf с переменным числом аргументов */ void minprintf(char *fmt, ...) { va_list ap; /* указывает на очередной безымянный аргумент */ char *p, *sval; int ival; double dval;

va_start(ap, fmt); /* устанавливает ap на 1-й безымянный аргумент */ for (p=fmt; *p; p++) { if (*p !='%') { putchar(*p); continue; } switch (*++p) { case 'd': ival = va_arg(ap, int); printf ("%d", ival); break; case 'f': dval = va_arg(ap, double); printf("%f", dval); break; case 's': for (sval = va_arg(ap, char *); *sval; sval++) putchar(*sval); break; default: putchar(*p); break; } } va_end(ap); /* очистка, когда все сделано */ }

Упражнение 7.3. Дополните minprintf другими возможностями printf.


Средство typedef


Язык Си предоставляет средство, называемое typedef, которое позволяет давать типам данных новые имена. Например, объявление

typedef int Length;

делает имя Length синонимом int. С этого момента тип Length можно применять в объявлениях, в операторе приведения и т. д. точно так же, как тип int:

Length len, maxlen; Length *lengths[];

Аналогично объявление

typedef char *String;

делает String синонимом char *, т. e. указателем на char, и правомерным будет, например, следующее его использование:

String р, lineptr[MAXLINES], alloc(int); int strcmp(String, String); p = (String) malloc(100);

Заметим, что объявляемый в typedef тип стоит на месте имени переменной в обычном объявлении, а не сразу за словом typedef. С точки зрения синтаксиса слово typedef напоминает класс памяти - extern, static и т. д. Имена типов записаны с заглавных букв для того, чтобы они выделялись.

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

typedef struct tnode *Treeptr;

typedef struct tnode { /* узел дерева: */ char *word; /* указатель на текст */ int count; /* число вхождений */ Treeptr left; /* левый сын */ Treeptr right; /* правый сын */ } Treenode;

В результате создаются два новых названия типов: Treenode (структура) и Treeptr (указатель на структуру). Теперь программу talloc можно записать в следующем виде:

Treeptr talloc(void) { return (Treeptr) malloc(sizeof(Treenode)); }

Следует подчеркнуть, что объявление typedef не создает объявления нового типа, оно лишь сообщает новое имя уже существующему типу. Никакого нового смысла эти новые имена не несут, они объявляют переменные в точности с теми же свойствами, как если бы те были объявлены напрямую без переименования типа. Фактически typedef аналогичен #define с тем лишь отличием, что при интерпретации компилятором он может справиться с такой текстовой подстановкой, которая не может быть обработана препроцессором. Например

typedef int (*PFI)(char *, char *);


создает тип PFI - "указатель на функцию (двух аргументов типа char *), возвращающую int", который, например, в программе сортировки, описанной в , можно использовать в таком контексте:

PFI strcmp, numcmp;

Помимо просто эстетических соображений, для применения typedef существуют две важные причины. Первая - параметризация программы, связанная с проблемой переносимости. Если с помощью typedef объявить типы данных, которые, возможно, являются машинно-зависимыми, то при переносе программы на другую машину потребуется внести изменения только в определения typedef. Одна из распространенных ситуаций - использование typedef-имен для варьирования целыми величинами. Для каждой конкретной машины это предполагает соответствующие установки short, int или long, которые делаются аналогично установкам стандартных типов, например size_t и ptrdiff_t.

Вторая причина, побуждающая к применению typedef,- желание сделать более ясным текст программы. Тип, названный Тreeptr (от английских слов tree - дерево и pointer - указатель), более понятен, чем тот же тип, записанный как указатель на некоторую сложную структуру.


Стандартный ввод-вывод


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

Простейший механизм ввода - это чтение одного символа из стандартного ввода (обычно с клавиатуры) функцией getchar:

int getchar(void)

В качестве результата каждого своего вызова функция getchar возвращает следующий символ ввода или, если обнаружен конец файла, EOF. Именованная константа EOF (аббревиатура от end of file - конец файла) определена в <stdio.h>. Обычно значение EOF равно -1, но, чтобы не зависеть от конкретного значения этой константы, обращаться к ней следует по имени (EOF).

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

prog < infile

предпишет программе prog читать символы из infile, а не с клавиатуры. Переключение ввода делается так, что сама программа prog не замечает подмены; в частности строка "< infile" не будет включена в аргументы командной строки argv. Переключение ввода будет также незаметным, если ввод исходит от другой программы и передается конвейерным образом. В некоторых системах командная строка

otherprog | prog

приведет к тому, что запустится две программы, otherprog и prog, и стандартный выход otherprog поступит на стандартный вход prog. Функция

int putchar(int)

используется для вывода: putchar(c) отправляет символ c в стандартный вывод, под которым по умолчанию подразумевается экран. Функция putchar в качестве результата возвращает посланный символ или, в случае ошибки, EOF. То же и в отношении вывода: с помощью записи вида > имя-файла вывод можно перенаправить в файл. Например, если prog использует для вывода функцию putchar, то

prog > outfile


будет направлять стандартный вывод не на экран, а в outfile. А командная строка

prog | anotherprog

соединит стандартный вывод программы prog со стандартным вводом программы anotherprog.

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

Любой исходный Си-файл, использующий хотя бы одну функцию библиотеки ввода-вывода, должен содержать в себе строку

#include <stdio.h>

причем она должна быть расположена до первого обращения к вводу-выводу. Если имя заголовочного файла заключено в угловые скобки < и >, это значит, что поиск заголовочного файла ведется в стандартном месте (например в системе UNIX это обычно директорий /usr/include).

Многие программы читают только из одного входного потока и пишут только в один выходной поток. Для организации ввода-вывода таким программам вполне хватит функций getchar, putchar и printf, а для начального обучения уж точно достаточно ознакомления с этими функциями. В частности, перечисленных функций достаточно, когда требуется вывод одной программы соединить с вводом следующей. В качестве примера рассмотрим программу lower, переводящую свой ввод на нижний регистр:

#include <stdio.h> #include <ctype.h>

main() /* lower: переводит ввод на нижний регистр */ { int с; while ((с = getchar()) != EOF) putchar(tolower(c)); return 0; }

Функция tolower определена в <ctype.h>. Она переводит буквы верхнего регистра в буквы нижнего регистра, а остальные символы возвращает без изменений. Как мы уже упоминали, "функции" вроде getchar и putchar из библиотеки <stdio.h> и функция tolower из библиотеки <ctype.h> часто реализуются в виде макросов, чтобы исключить накладные расходы от вызова функции на каждый отдельный символ. В мы покажем, как это делается. Независимо от того, как на той или иной машине реализованы функции библиотеки <ctype.h>, использующие их программы могут ничего не знать о кодировке символов.

Упражнение 7.1. Напишите программу, осуществляющую перевод ввода с верхнего регистра на нижний или с нижнего на верхний в зависимости от имени, по которому она вызывается и текст которого находится в arg[0].


Статические переменные


Переменные sp и val в файле stack.с, а также buf и bufp в getch.с находятся в личном пользовании функций этих файлов, и нет смысла открывать к ним доступ кому-либо еще. Указание static, примененное к внешней переменной или функции, ограничивает область видимости соответствующего объекта концом файла. Это способ скрыть имена. Так, переменные buf и bufp должны быть внешними, поскольку их совместно используют функции getch и ungetch, но их следует сделать невидимыми для "пользователей" функций getch и ungetch.

Статическая память специфицируется словом static, которое помещается перед обычным объявлением. Если рассматриваемые нами две функции и две переменные компилируются в одном файле, как в показанном ниже примере:

static char buf[BUFSIZE]; /* буфер для ungetch */ static int bufp = 0; /* след. свободная позиция в buf */

int getch(void) {...}

void ungetch(int с) {...}

то никакая другая программа не будет иметь доступ ни к buf, ни к bufp, и этими именами можно свободно пользоваться в других файлах для совсем иных целей. Точно так же, помещая указание static перед объявлениями переменных sp и val, с которыми работают только push и pop, мы можем скрыть их от остальных функций.

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

Объявление static можно использовать и для внутренних переменных. Как и автоматические переменные, внутренние статические переменные локальны в функциях, но в отличие от автоматических, они не возникают только на период работы функции, а существуют постоянно. Это значит, что внутренние статические переменные обеспечивают постоянное сохранение данных внутри функции.

Упражнение 4.11. Модифицируйте функцию getop так, чтобы отпала необходимость в функции ungetch. Подсказка: используйте внутреннюю статическую переменную.



Структуры и функции


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

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

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

/* makepoint: формирует точку по компонентам x и y */ struct point makepoint(int х, int у) { struct point temp;

temp.x = х; temp.у = у; return temp; }

Заметим: никакого конфликта между именем аргумента и именем элемента структуры не возникает; более того, сходство подчеркивает родство обозначаемых им объектов.

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

struct rect screen; struct point middle; struct point makepoint(int, int);

screen.pt1 = makepoint(0, 0); screen.pt2 = makepoint(XMAX, YMAX); middle = makepoint((screen.pt1.x + screen.pt2.x)/2, (screen.pt1.y + screen.pt2.y)/2);

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

/* addpoint: сложение двух точек */ struct point addpoint(struct point p1, struct point p2) { p1.x += p2.x; p1.y += p2.y; return p1; }

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


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

/* ptinrect: возвращает 1, если p в r, и 0 в противном случае */ int ptinrect(struct point р, struct rect r) { return p.x >= r.pt1.x && p.x < r.pt2.x && p.y >= r.pt1.y && p.y < r.pt2.y; }

Здесь предполагается, что прямоугольник представлен в стандартном виде, т.е. координаты точки pt1 меньше соответствующих координат точки pt2. Следующая функция гарантирует получение прямоугольника в каноническом виде.

#define min(a, b) ((a) < (b) ? (a) : (b)) #define max(a, b) ((a) > (b) ? (a) : (b))

/* canonrect: канонизация координат прямоугольника */ struct rect canonrect(struct rect r) { struct rect temp;

temp.pt1.x = min(r.pt1.x, r.pt2.x); temp.ptl.y = min(r.pt1.y, r.pt2.у); temp.pt2.x = max(r.pt1.x, r.pt2.x); temp.pt2.y = max(r.pt1.y, r.pt2.y); return temp; }

Если функции передается большая структура, то, чем копировать ее целиком, эффективнее передать указатель на нее. Указатели на структуры ничем не отличаются от указателей на обычные переменные. Объявление

struct point *pp;

сообщает, что pp - это указатель на структуру типа struct point. Если pp указывает на структуру point, то *pp - это сама структура, а (*pp).x и (*pp).y - ее элементы. Используя указатель pp, мы могли бы написать

struct point origin, *pp;

pp = &origin; printf("origin: (%d,%d)\n", (*pp).x, (*pp).y);

Скобки в (*pp).x необходимы, поскольку приоритет оператора . выше, чем приоритет *. Выражение *pp.x будет проинтерпретировано как *(pp.x), что неверно, поскольку pp.x не является указателем.

Указатели на структуры используются весьма часто, поэтому для доступа к ее элементам была придумана еще одна, более короткая форма записи. Если p — указатель на структуру, то

р->элемент-структуры

есть ее отдельный элемент. (Оператор -> состоит из знака -, за которым сразу следует знак >.) Поэтому printf можно переписать в виде



printf("origin: (%d,%d)\n", pp->х, pp->y);

Операторы . и -> выполняются слева направо. Таким образом, при наличии объявления

struct rect r, *rp = &r;

следующие четыре выражения будут эквивалентны:

r.pt1.x rp->pt1.x (r.pt1).x (rp->pt1).x

Операторы доступа к элементам структуры . и -> вместе с операторами вызова функции () и индексации массива [] занимают самое высокое положение в иерархии приоритетов и выполняются раньше любых других операторов. Например, если задано объявление

struct { int len; char *str; } *p;

то

++p->len

увеличит на 1 значение элемента структуры len, а не указатель p, поскольку в этом выражении как бы неявно присутствуют скобки: ++(p->len). Чтобы изменить порядок выполнения операций, нужны явные скобки. Так, в (++р)->len, прежде чем взять значение len, программа прирастит указатель p. В (р++)->len указатель p увеличится после того, как будет взято значение len (в последнем случае скобки не обязательны).

По тем же правилам *p->str обозначает содержимое объекта, на который указывает str; *p->str++ прирастит указатель str после получения значения объекта, на который он указывал (как и в выражении *s++), (*p->str)++ увеличит значение объекта, на который указывает str; *p++->str увеличит p после получения того, на что указывает str.


Структуры со ссылками на себя


Предположим, что мы хотим решить более общую задачу - написать программу, подсчитывающую частоту встречаемости для любых слов входного потока. Так как список слов заранее не известен, мы не можем предварительно упорядочить его и применить бинарный поиск. Было бы неразумно пользоваться и линейным поиском каждого полученного слова, чтобы определять, встречалось оно ранее или нет - в этом случае программа работала бы слишком медленно. (Более точная оценка: время работы такой программы пропорционально квадрату количества слов.) Как можно организовать данные, чтобы эффективно справиться со списком произвольных слов?

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

В дереве на каждое отдельное слово предусмотрен "узел", который содержит:

- указатель на текст слова; - счетчик числа встречаемости; - указатель на левый сыновний узел; - указатель на правый сыновний узел.

У каждого узла может быть один или два сына, или узел вообще может не иметь сыновей.

Узлы в дереве располагаются так, что по отношению к любому узлу левое поддерево содержит только те слова, которые лексикографически меньше, чем слово данного узла, а правое - слова, которые больше него. Вот как выглядит дерево, построенное для фразы "now is the time for all good men to come to the aid of their party" ("настало время всем добрым людям помочь своей партии"), по завершении процесса, в котором для каждого нового слова в него добавлялся новый узел:

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


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

struct tnode { /* узел дерева */ char *word; /* указатель на текст */ int count; /* число вхождений */ struct tnode *left; /* левый сын */ struct tnode *right; /* правый сын */ };

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

struct tnode *left;

объявляет left как указатель на tnode, а не сам tnode.

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

struct t { ... struct s *p; /* р указывает на s */ }; struct s { ... struct t *q; /* q указывает на t */ }

Вся программа удивительно мала - правда, она использует вспомогательные программы вроде getword, уже написанные нами. Главная программа читает слова с помощью getword и вставляет их в дерево посредством addtree.

#include <stdio.h> #include <ctype.h> #include <string.h>

#define MAXWORD 100

struct tnode *addtree(struct tnode *, char *); void treeprint(struct tnode *); int getword(char *, int);

/* подсчет частоты встречаемости слов */ main() { struct tnode *root; char word[MAXWORD];

root = NULL; while (getword (word, MAXWORD) != EOF) if (isalpha(word[0])) root = addtree(root, word); treeprint(root); return 0; }

Функция addtree рекурсивна. Первое слово функция main помещает на верхний уровень дерева (корень дерева). Каждое вновь поступившее слово сравнивается со словом узла и "погружается" или в левое, или в правое поддерево с помощью рекурсивного обращения к addtree. Через некоторое время это слово обязательно либо совпадет с каким-нибудь из имеющихся в дереве слов (в этом случае к счетчику будет добавлена 1), либо программа встретит пустую позицию, что послужит сигналом для создания нового узла и добавления его к дереву. Создание нового узла сопровождается тем, что addtree возвращает на него указатель, который вставляется в узел родителя.

struct tnode *talloc(void); char *strdup(char *);



/* addtree: добавляет узел со словом w в р или ниже него */ struct tnode *addtree(struct tnode *p, char *w) { int cond;

if (р == NULL) { /* слово встречается впервые */ p = talloc(); /* создается новый узел */ p->word = strdup(w); p->count = 1; p->left = p->right = NULL; } else if ((cond = strcmp(w, p->word)) == 0) p->count++; /* это слово уже встречалось */ else if (cond < 0) /* меньше корня левого поддерева */ p->left = addtree(p->left, w); else /* больше корня правого поддерева */ p->right = addtree(p->right, w); return p; }

Память для нового узла запрашивается с помощью программы talloc, которая возвращает указатель на свободное пространство, достаточное для хранения одного узла дерева, а копирование нового слова в отдельное место памяти осуществляется с помощью strdup. (Мы рассмотрим эти программы чуть позже.) В тот (и только в тот) момент, когда к дереву подвешивается новый узел, происходит инициализация счетчика и обнуление указателей на сыновей. Мы опустили (что неразумно) контроль ошибок, который должен выполняться при получении значений от strdup и talloc.

Функция treeprint печатает дерево в лексикографическом порядке: для каждого узла она печатает его левое поддерево (все слова, которые меньше слова данного узла), затем само слово и, наконец, правое поддерево (слова, которые больше слова данного узла).

/* treeprint: упорядоченная печать дерева р */ void treeprint(struct tnode *p) { if (p != NULL) { treeprint (p->left); printf("%4d %s\n", p->count, p->word); treeprint(p->right); } }

Если вы не уверены, что досконально разобрались в том, как работает рекурсия, "проиграйте" действия treeprint на дереве, приведенном выше. Практическое замечание: если дерево "несбалансировано" (что бывает, когда слова поступают не в случайном порядке), то время работы программы может сильно возрасти. Худший вариант, когда слова уже упорядочены; в этом случае затраты на вычисления будут такими же, как при линейном поиске. Существуют обобщения бинарного дерева, которые не страдают этим недостатком, но здесь мы их не описываем.



Прежде чем завершить обсуждение этого примера, сделаем краткое отступление от темы и поговорим о механизме запроса памяти. Очевидно, хотелось бы иметь всего лишь одну функцию, выделяющую память, даже если эта память предназначается для разного рода объектов. Но если одна и та же функция обеспечивает память, скажем) и для указателей на char, и для указателей на struct tnode, то возникают два вопроса. Первый: как справиться с требованием большинства машин, в которых объекты определенного типа должны быть выровнены (например, int часто должны размещаться, начиная с четных адресов)? И второе: как объявить функцию-распределитель памяти, которая вынуждена в качестве результата возвращать указатели разных типов?

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

Вопрос об объявлении типа таких функций, как malloc, является камнем преткновения в любом языке с жесткой проверкой типов. В Си вопрос решается естественным образом: malloc объявляется как функция, которая возвращает указатель на void. Полученный указатель затем явно приводится к желаемому типу (Замечание о приведении типа величины, возвращаемой функцией malloc, нужно переписать. Пример коpректен и работает, но совет является спорным в контексте стандартов ANSI/ISO 1988-1989 г. На самом деле это не обязательно (при условии что приведение void* к ALMOSTANYTYPE* выполняется автоматически) и возможно даже опасно, если malloc или ее заместитель не может быть объявлен как функция, возвращающая void*. Явное приведение типа может скрыть случайную ошибку. В другие времена (до появления стандарта ANSI) приведение считалось обязательным, что также справедливо и для C++. — Примеч. авт.). Описания malloc и связанных с ней функций находятся в стандартном заголовочном файле <stdlib.h>. Таким образом, функцию talloc можно записать так:

#include <stdlib.h>



/* talloc: создает tnode */ struct tnode *talloc(void) { return (struct tnode *) malloc(sizeof(struct tnode)); }

Функция strdup просто копирует строку, указанную в аргументе, в место, полученное с помощью malloc:

char *strdup(char *s) /* делает дубликат s */ { char *p;

p = (char *) malloc(strlen(s)+1); /* +1 для '\0' */ if (p != NULL) strcpy(p, s); return p; }

Функция malloc возвращает NULL, если свободного пространства нет; strdup возвращает это же значение, оставляя заботу о выходе из ошибочной ситуации вызывающей программе.

Память, полученную с помощью malloc, можно освободить для повторного использования, обратившись к функции free (см. и ).

Упражнение 6.2. Напишите программу, которая читает текст Си-программы и печатает в алфавитном порядке все группы имен переменных, в которых совпадают первые 6 символов, но последующие в чем-то различаются. Не обрабатывайте внутренности закавыченных строк и комментариев. Число 6 сделайте параметром, задаваемым в командной строке.

Упражнение 6.3. Напишите программу печати таблицы "перекрестных ссылок", которая будет печатать все слова документа и указывать для каждого из них номера строк, где оно встретилось. Программа должна игнорировать "шумовые" слова, такие как "и", "или" и пр.

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


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


В Си существует всего лишь несколько базовых типов:

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

int - целое, обычно отображающее естественное представление целых в машине;

float - число с плавающей точкой одинарной точности;

double - число с плавающей точкой двойной точности.

Имеется также несколько квалификаторов, которые можно использовать вместе с указанными базовыми типами. Например, квалификаторы short (короткий) и long (длинный) применяются к целым:

short int sh; long int counter;

В таких объявлениях слово int можно опускать, что обычно и делается. Если только не возникает противоречий со здравым смыслом, short int и long int должны быть разной длины, а int соответствовать естественному размеру целых на данной машине. Чаще всего для представления целого, описанного с квалификатором short, отводится 16 бит, с квалификатором long - 32 бита, а значению типа int - или 16, или 32 бита. Разработчики компилятора вправе сами выбирать подходящие размеры, сообразуясь с характеристиками своего компьютера и соблюдая следующие ограничения: значения типов short и int представляются по крайней мере 16 битами; типа long - по крайней мере 32 битами; размер short не больше размера int, который в свою очередь не больше размера long.

Квалификаторы signed (со знаком) или unsigned (без знака) можно применять к типу char и любому целочисленному типу. Значения unsigned всегда положительны или равны нулю и подчиняются законам арифметики по модулю 2n, где n - количество бит в представлении типа. Так, если значению char отводится 8 битов, то unsigned char имеет значения в диапазоне от 0 до 255, a signed char – от -128 до 127 (в машине с двоичным дополнительным кодом). Являются ли значения типа просто char знаковыми или беззнаковыми, зависит от реализации, но в любом случае коды печатаемых символов положительны.

Тип long double предназначен для арифметики с плавающей точкой повышенной точности. Как и в случае целых, размеры объектов с плавающей точкой зависят от реализации; float, double и long double могут представляться одним размером, а могут - двумя или тремя разными размерами.

Именованные константы для всех размеров вместе с другими характеристиками машины и компилятора содержатся в стандартных заголовочных файлах <limits.h> и <float.h> (см. приложение B).

Упражнение 2.1. Напишите программу, которая будет выдавать диапазоны значений типов char, short, int и long, описанных как signed и как unsigned, с помощью печати соответствующих значений из стандартных заголовочных файлов и путем прямого вычисления. Определите диапазоны чисел с плавающей точкой различных типов. Вычислить эти диапазоны сложнее.



Указатели и адреса


Начнем с того, что рассмотрим упрощенную схему организации памяти. Память типичной машины подставляет собой массив последовательно пронумерованных или проадресованных ячеек, с которыми можно работать по отдельности или связными кусками. Применительно к любой машине верны следующие утверждения: один байт может хранить значение типа char, двухбайтовые ячейки могут рассматриваться как целое типа short, а четырехбайтовые - как целые типа long. Указатель - это группа ячеек (как правило, две или четыре), в которых может храниться адрес. Так, если c имеет тип char, а p - указатель на c, то ситуация выглядит следующим образом:

Унарный оператор & выдает адрес объекта, так что инструкция

p = &c;

присваивает переменной p адрес ячейки c (говорят, что p указывает на c). Оператор & применяется только к объектам, расположенным в памяти: к переменным и элементам массивов. Его операндом не может быть ни выражение, ни константа, ни регистровая переменная.

Унарный оператор * есть оператор косвенного доступа. Примененный к указателю он выдает объект, на который данный указатель указывает. Предположим, что x и y имеют тип int, а ip – укаэатель на int. Следующие несколько строк придуманы специально для того, чтобы показать, каким образом объявляются указатели и как используются операторы & и *.

int х = 1, у = 2, z[10]; int *ip; /* ip - указатель на int */

ip = &x; /* теперь ip указывает на x */ y = *ip; /* y теперь равен 1 */ *ip = 0; /* x теперь равен 0 */ ip = &z[0]; /* ip теперь указывает на z[0] */

Объявления x, y и z нам уже знакомы. Объявление указателя ip

int *ip;

мы стремились сделать мнемоничным - оно гласит: "выражение *ip имеет тип int". Синтаксис объявления переменной "подстраивается" под синтаксис выражений, в которых эта переменная может встретиться. Указанный принцип применим и в объявлениях функций. Например, запись

double *dp, atof (char *);

означает, что выражения *dp и atof(s) имеют тип double, а аргумент функции atof есть указатель на char.


Вы, наверное, заметили, что указателю разрешено указывать только на объекты определенного типа. (Существует одно исключение: "указатель на void" может указывать на объекты любого типа, но к такому указателю нельзя применять оператор косвенного доступа. Мы вернемся к этому в .)

Если ip указывает на x целочисленного типа, то *ip можно использовать в любом месте, где допустимо применение x; например,

*ip = *ip + 10;

увеличивает *ip на 10.

Унарные операторы * и & имеют более высокий приоритет, чем арифметические операторы, так что присваивание

y = *ip + 1;

берет то, на что указывает ip, и добавляет к нему 1, а результат присваивает переменной y. Аналогично

*ip += 1;

увеличивает на единицу то, на что указывает ip; те же действия выполняют

++*ip;

и

(*iр)++;

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

И наконец, так как указатели сами являются переменными, в тексте они могут встречаться и без оператора косвенного доступа. Например, если iq есть другой указатель на int, то

iq = ip;

копирует содержимое ip в iq, чтобы ip и iq указывали на один и тот же объект.


Указатели и аргументы функций


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

swap(a, b);

где функция swap определена следующим образом:

void swap(int х, int у) /* НЕВЕРНО */ { int temp; temp = х; x = y; у = temp; }

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

swap(&a, &b);

Так как оператор & получает адрес переменной, &a есть указатель на a. В самой же функции swap параметры должны быть объявлены как указатели, при этом доступ к значениям параметров будет осуществляться косвенно.

void swap(int *px, int *py) /* перестановка *px и *py */ { int temp; temp = *рх; *рх = *py; *ру = temp; }

Графически это выглядит следующим образом: в вызывающей программе:

Аргументы-указатели позволяют функции осуществлять доступ к объектам вызвавшей ее программы и дают возможность изменить эти объекты. Рассмотрим, например, функцию getint, которая осуществляет ввод в свободном формате одного целого числа и его перевод из текстового представления в значение типа int. Функция getint должна возвращать значение полученного числа или сигнализировать значением EOF о конце файла, если входной поток исчерпан. Эти значения должны возвращаться по разным каналам, так как нельзя рассчитывать на то, что полученное в результате перевода число никогда не совпадет с EOF.

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

int n, array[SIZE], getint (int *);


for (n = 0; n < SIZE && getint (&array[n]) != EOF; n++) ;

Результат каждого очередного обращения к getint посылается в array[n], и n увеличивается на единицу. Заметим, и это существенно, что функции getint передается адрес элемента array[n]. Если этого не сделать, у getint не будет способа вернуть в вызывающую программу переведенное целое число.

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

#include <ctype.h>

int getch (void); void ungetch (int);

/* getint: читает следующее целое из ввода в *pn */ int getint(int *pn) { int c, sign;

while (isspace(c = getch())) ; /* пропуск символов-разделителей */

if(!isdigit(c) && c != EOF && c != '+' && c != '-') { ungetch (c); /* не число */ return 0; } sign =(c =='-') ? -1 : 1; if (с == '+' с == '-') с = getch(); for (*pn = 0; isdigit(c); c = getch()) *pn = 10 * *pn + (c -'0'); *pn *= sign; if (c!= EOF) ungetch(c); return c; }

Везде в getint под *pn подразумевается обычная переменная типа int. Функция ungetch вместе с getch () включена в программу, чтобы обеспечить возможность отослать назад лишний прочитанный символ.

Упражнение 5.1. Функция getint написана так, что знаки - или +, за которыми не следует цифра, она понимает как "правильное" представление нуля. Скорректируйте программу таким образом, чтобы в подобных случаях она возвращала прочитанный знак назад во ввод.

Упражнение 5.2. Напишите функцию getfloat - аналог getint для чисел с плавающей точкой. Какой тип будет иметь результирующее значение, задаваемое функцией getfloat?


Указатели и массивы


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

Объявление

int a[10];

Определяет массив a размера 10, т. е. блок из 10 последовательных объектов с именами a[0], a[1], ..., a[9].

Запись a[i] отсылает нас к i-му элементу массива. Если pa есть указатель на int, т. е. объявлен как

int *pa;

то в результате присваивания

pa = &a[0];

pa будет указывать на нулевой элемент a, иначе говоря, pa будет содержать адрес элемента a[0].

Теперь присваивание

x = *pa;

будет копировать содержимое a[0] в x.

Если pa указывает на некоторый элемент массива, то pa+1 по определению указывает на следующий элемент, pa+i - на i-й элемент после pa, a pa-i - на i-й элемент перед pa. Таким образом, если pa указывает на a[0], то

*(pa+1)

есть содержимое a[1], a+i - адрес a[i], a *(pa+i) - содержимое a[i].

Сделанные замечания верны безотносительно к типу и размеру элементов массива a. Смысл слов "добавить 1 к указателю", как и смысл любой арифметики с указателями, состоит в том, чтобы pa+1 указывал на следующий объект, a pa+i - на i-й после pa.

Между индексированием и арифметикой с указателями существует очень тесная связь. По определению значение переменной или выражения типа массив есть адрес нулевого элемента массива. После присваивания

pa = &a[0];

ра и a имеют одно и то же значение. Поскольку имя массива является синонимом расположения его начального элемента, присваивание pa=&a[0] можно также записать в следующем виде:

pa = a;

Еще более удивительно (по крайней мере на первый взгляд) то, что a[i] можно записать как *(a+i). Вычисляя a[i], Си сразу преобразует его в *(a+i); указанные две формы записи эквивалентны. Из этого следует, что полученные в результате применения оператора & записи &a[i] и a+i также будут эквивалентными, т. е. и в том и в другом случае это адрес i-го элемента после a. С другой стороны, если pa - указатель, то его можно использовать с индексом, т. е. запись pa[i] эквивалентна записи *(pa+i). Короче говоря, элемент массива можно изображать как в виде указателя со смещением, так и в виде имени массива с индексом.


Между именем массива и указателем, выступающим в роли имени массива, существует одно различие. Указатель - это переменная, поэтому можно написать pa=a или pa++. Но имя массива не является переменной, и записи вроде a=pa или a++ не допускаются.

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

/* strlen: возвращает длину строки */ int strlen(char *s) { int n; for (n = 0; *s != '\0'; s++) n++; return n; }

Так как переменная s - указатель, к ней применима операция ++; s++ не оказывает никакого влияния на строку символов функции, которая обратилась к strlen. Просто увеличивается на 1 некоторая копия указателя, находящаяся в личном пользовании функции strlen. Это значит, что все вызовы, такие как:

strlen("3дравствуй, мир"); /* строковая константа */ strlen(array); /* char array[100]; */ strlen(ptr); /* char *ptr; */

правомерны.

Формальные параметры

char s[];

и

char *s;

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

Функции можно передать часть массива, для этого аргумент должен указывать на начало подмассива. Например, если a - массив, то в записях

f(&a[2])

или

f(a+2)

функции f передается адрес подмассива, начинающегося с элемента a[2]. Внутри функции f описание параметров может выглядеть как

f(int arr[]) {...}

или

f(int *arr) {...}

Следовательно, для f тот факт, что параметр указывает на часть массива, а не на весь массив, не имеет значения.

Если есть уверенность, что элементы массива существуют, то возможно индексирование и в "обратную" сторону по отношению к нулевому элементу; выражения p[-1], p[-2] и т.д. не противоречат синтаксису языка и обращаются к элементам, стоящим непосредственно перед p[0]. Разумеется, нельзя "выходить" за границы массива и тем самым обращаться к несуществующим объектам.


Указатели на функции


В Си сама функция не является переменной, но можно определить указатель на функцию и работать с ним, как с обычной переменной: присваивать, размещать в массиве, передавать в качестве параметра функции, возвращать как результат из функции и т. д. Для иллюстрации этих возможностей воспользуемся программой сортировки, которая уже встречалась в настоящей главе. Изменим ее так, чтобы при задании необязательного аргумента -n вводимые строки упорядочивались по их числовому значению, а не в лексикографическом порядке.

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

Лексикографическое сравнение двух строк выполняется функцией strcmp (мы уже использовали эту функцию в ранее рассмотренной программе сортировки); нам также потребуется программа numcmp, сравнивающая две строки как числовые значения и возвращающая результат сравнения в том же виде, в каком его выдает strcmp. Эти функции объявляются перед main, а указатель на одну из них передается функции qsort. Чтобы сосредоточиться на главном, мы упростили себе задачу, отказавшись от анализа возможных ошибок при задании аргументов.

#include <stdio.h> #include <string.h>

#define MAXLINES 5000 /* максимальное число строк */ char *lineptr[MAXLINES]; /* указатели на строки текста */

int readlines(char *lineptr[], int nlines); void writelines(char *lineptr[], int nlines); void qsort(void *lineptr[], int left, int right, int (*comp)(void *, void *)); int numcmp(char *, char *);

/* сортировка строк */ main(int argc, char *argv[]) { int nlines; /* количество прочитанных строк */ int numeric = 0; /* 1, если сорт. по числ. знач. */ if (argc > 1 && strcmp(argv[1], "-n") == 0) numeric = 1; if ((nlines = readlines(lineptr, MAXLINES)) >= 0) { qsort((void **) lineptr, 0, nlines-1, (int (*)(void*,void*))(numeric ? numcmp : strcmp)); writelines(lineptr, nlines); return 0; } else { printf("Bведено слишком много строк\n"); return 1; } }


В обращениях к функциям qsort, strcmp и numcmp их имена трактуются как адреса этих функций, поэтому оператор & перед ними не нужен, как он не был нужен и перед именем массива.

Мы написали qsort так, чтобы она могла обрабатывать данные любого типа, а не только строки символов. Как видно из прототипа, функция qsort в качестве своих аргументов ожидает массив указателей, два целых значения и функцию с двумя аргументами-указателями. В качестве аргументов-указателей заданы указатели обобщенного типа void *. Любой указатель можно привести к типу void * и обратно без потери информации, поэтому мы можем обратиться к qsort, предварительно преобразовав аргументы в void *. Внутри функции сравнения ее аргументы будут приведены к нужному ей типу. На самом деле эти преобразования никакого влияния на представления аргументов не оказывают, они лишь обеспечивают согласованность типов для компилятора.

/* qsort: сортирует v[left]...v[right] по возрастанию */ void qsort(void *v[], int left, int right, int (*comp)(void *, void *)) { int i, last; void swap(void *v[], int, int);

if (left >= right) /* ничего не делается, если */ return; /* в массиве менее двух элементов */ swap(v, left, (left + right)/2); last = left; for (i = left+1; i

Повнимательней приглядимся к объявлениям. Четвертый параметр функции qsort:

int (*comp)(void *, void *)

сообщает, что comp - это указатель на функцию, которая имеет два аргумента- указателя и выдает результат типа int. Использование comp в строке

if ((*comp)(v[i], v[left]) < 0)

согласуется с объявлением "comp - это указатель на функцию", и, следовательно, *comp - это функция, а

(*comp)(v[i], v[left])

- обращение к ней. Скобки здесь нужны, чтобы обеспечить правильную трактовку объявления; без них объявление

int *comp(void *, void *) /* НЕВЕРНО */

говорило бы, что comp - это функция, возвращающая указатель на int, а это совсем не то, что требуется.

Мы уже рассматривали функцию strcmp, сравнивающую две строки. Ниже приведена функция numcmp, которая сравнивает две строки, рассматривая их как числа; предварительно они переводятся в числовые значения функцией atof.



#include <stdlib.h>

/* numcmp: сравнивает s1 и s2 как числа */

int numcmp(char *s1, char *s2) { double v1, v2;

v1 = atof(s1); v2 = atof(s2); if (v1 < v2) return -1; else if (v1 > v2) return 1; else return 0; }

Функция swap, меняющая местами два указателя, идентична той, что мы привели ранее в этой главе за исключением того, что объявления указателей заменены на void*.

void swap(void *v[], int i, int j) { void *temp; temp = v[i]; v[i] = v[j]; v[j] = temp; }

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

Упражнение 5.14. Модифицируйте программу сортировки, чтобы она реагировала на параметр -r, указывающий, что объекты нужно сортировать в обратном порядке, т. е. в порядке убывания. Обеспечьте, чтобы -r работал и вместе с -n.

Упражнение 5.15. Введите в программу необязательный параметр -f, задание которого делало бы неразличимыми символы нижнего и верхнего регистров (например, a и A должны оказаться при сравнении равными).

Упражнение 5.16. Предусмотрите в программе необязательный параметр -d, который заставит программу при сравнении учитывать только буквы, цифры и пробелы. Организуйте программу таким образом, чтобы этот параметр мог работать вместе с параметром -f.

Упражнение 5.17. Реализуйте в программе возможность работы с полями: возможность сортировки по полям внутри строк. Для каждого поля предусмотрите свой набор параметров. Предметный указатель этой книги (Имеется в виду оригинал книги на английским языке. – Примеч. пер.) упорядочивался с параметрами: -df для терминов и -n для номеров страниц.


Указатели на структуры


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

Внешнее объявление массива keytab остается без изменения, a main и binsearch нужно модифицировать.

#include <stdio.h> #include <ctype.h> #include <string.h> #define MAXWORD 100

int getword(char *, int); struct key *binsearch(char *, struct key *, int);

/* подсчет ключевых слов Си: версия с указателями */ main() { char word[MAXWORD]; struct key *p; while (getword(word, MAXWORD) != EOF) if (isalpha(word[0])) if ((p = binsearch(word, keytab, NKEYS)) != NULL) p->count++; for (p = keytab; p < keytab + NKEYS; p++) if (p->count > 0) printf("%4d %s\n", p->count, p->word); return 0; }

/* binsearch: найти слово word в tab[0]...tab[n-1] */ struct key *binsearch(char *word, struct key *tab, int n) { int cond; struct key *low = &tab[0]; struct key *high = &tab[n]; struct key *mid; while (low < high) { mid = low + (high - low) / 2; if ((cond = strcmp(word, mid->word)) < 0) high = mid; else if (cond > 0) low = mid + 1; else return mid; } return NULL; }

Некоторые детали этой программы требуют пояснений. Во-первых, описание функции binsearch должно отражать тот факт, что она возвращает указатель на struct key, а не целое, это объявлено как в прототипе функции, так и в функции binsearch. Если binsearch находит слово, то она выдает указатель на него, в противном случае она возвращает NULL. Во-вторых, к элементам keytab доступ в нашей программе осуществляется через указатели. Это потребовало значительных изменений в binsearch. Инициализаторами для low и high теперь служат указатели на начало и на место сразу после конца массива. Вычисление положения среднего элемента с помощью формулы

mid = (low + high) / 2 /* НЕВЕРНО */

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

mid = low + (high-low) / 2


превратит mid в указатель на элемент, лежащий посередине между low и high.

Самое важное при переходе на новый вариант программы - сделать так, чтобы не генерировались неправильные указатели и не было попыток обратиться за пределы массива. Проблема в том, что и &tab[-1], и &tab[n] находятся вне границ массива. Первый адрес определенно неверен, нельзя также осуществить доступ и по второму адресу. По правилам языка, однако, гарантируется, что адрес ячейки памяти, следующей сразу за концом массива (т. е. &tab[n]), в арифметике с указателями воспринимается правильно.

В главной программе main мы написали

for (р = keytab; р < keytab + NKEYS; р++)

Если p - это указатель на структуру, то при выполнении операций с р учитывается размер структуры. Поэтому р++ увеличит р на такую величину, чтобы выйти на следующий структурный элемент массива, а проверка условия вовремя остановит цикл.

Не следует, однако, полагать, что размер структуры равен сумме размеров ее элементов. Вследствие выравнивания объектов разной длины в структуре могут появляться безымянные "дыры". Например, если переменная типа char занимает один байт, а int - четыре байта, то для структуры

struct { char с; int i; };

может потребоваться восемь байтов, а не пять. Оператор sizeof возвращает правильное значение.

Наконец, несколько слов относительно формата программы. Если функция возвращает значение сложного типа, как, например, в нашем случае она возвращает указатель на структуру:

struct key *binsearch(char *word, struct key *tab, int n)

то "высмотреть" имя функции оказывается совсем не просто. В подобных случаях иногда пишут так:

struct key * binsearch(char *word, struct key *tab, int n)

Какой форме отдать предпочтение - дело вкуса. Выберите ту, которая больше всего вам нравится, и придерживайтесь ее.


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


Начинающие программировать на Си иногда не понимают, в чем разница между двумерным массивом и массивом указателей вроде name из приведенного примера. Для двух следующих определений:

int a[10][20]; int *b[10];

записи a[3][4] и b[3][4] будут синтаксически правильным обращением к некоторому значению типа int. Однако только a является истинно двумерным массивом: для двухсот элементов типа int будет выделена память, а вычисление смещения элемента a[строка][столбец] от начала массива будет вестись по формуле 20 * строка + столбец, учитывающей его прямоугольную природу. Для b же определено только 10 указателей, причем без инициализации. Инициализация должна задаваться явно -либо статически, либо в программе. Предположим, что каждый элемент b указывает на двадцатиэлементный массив, в результате где-то будут выделены пространство, в котором разместятся 200 значений типа int, и еще 10 ячеек для указателей. Важное преимущество массива указателей в том, что строки такого массива могут иметь разные длины. Таким образом, каждый элемент массива b не обязательно указывает на двадцатиэлементный вектор; один может указывать на два элемента, другой - на пятьдесят, а некоторые и вовсе могут ни на что не указывать.

Наши рассуждения здесь касались целых значений, однако чаще массивы указателей используются для работы со строками символов, различающимися по длине, как это было в функции month_name. Сравните определение массива указателей и соответствующий ему рисунок:

char *name[] = {"Неправильный месяц", "Янв", "Февр", "Март"};

с объявлением и рисунком для двумерного массива:

char aname[][15] = {"Неправ. месяц", "Янв", "Февр", "Март"};

Упражнение 5.9. Перепишите программы day_of_year и month_day, используя вместо индексов указатели.



Управление ошибками (stderr и exit)


Обработку ошибок в cat нельзя признать идеальной. Беда в том, что если файл по какой-либо причине недоступен, сообщение об этом мы получим по окончании конкатенируемого вывода. Это нас устроило бы, если бы вывод отправлялся только на экран, a не в файл или по конвейеру другой программе. Чтобы лучше справиться с этой проблемой, программе помимо стандартного вывода stdout придается еще один выходной поток, называемый stderr. Вывод в stderr обычно отправляется на экран, даже если вывод stdout перенаправлен в другое место. Перепишем cat так, чтобы сообщения об ошибках отправлялись в stderr.

#include <stdio.h> /* cat: конкатенация файлов, версия 2 */ main(int argc, char *argv[]) { FILE *fp; void filecopy(FILE *, FILE *); char *prog = argv[0]; /* имя программы */ if (argc == 1) /* нет аргументов, копируется станд. ввод */ filecopy(stdin, stdout); else while (--argc > 0) if ((fp = fopen(*++argv, "r")) == NULL) { fprintf(stderr, "%s: не могу открыть файл %s\n", prog, *argv); exit(1); } else { filecopy(fp, stdout); fclose(fp); } if (ferror(stdout)) { fprintf(stderr, "%s: ошибка записи в stdout\n", prog); exit(2); } exit(0); }

Программа сигнализирует об ошибках двумя способами. Первый - сообщение об ошибке с помощью fprintf посылается в stderr с тем, чтобы оно попало на экран, а не оказалось на конвейере или в другом файле вывода. Имя программы, хранящееся в argv[0], мы включили в сообщение, чтобы в случаях, когда данная программа работает совместно с другими, был ясен источник ошибки.

Второй способ указать на ошибку - обратиться к библиотечной функции exit, завершающей работу программы. Аргумент функции exit доступен некоторому процессу, вызвавшему данный процесс. А следовательно, успешное или ошибочное завершение программы можно проконтролировать с помощью некоей программы, которая рассматривает эту программу в качестве подчиненного процесса. По общей договоренности возврат нуля сигнализирует о том, что работа прошла нормально, в то время как ненулевые значения обычно говорят об ошибках. Чтобы опорожнить буфера, накопившие информацию для всех открытых файлов вывода, функция exit вызывает fclose.


Инструкция return exp главной программы main эквивалентна обращению к функции exit(exp). Последний вариант (с помощью exit) имеет то преимущество, что он пригоден для выхода и из других функций, и, кроме того, слово exit легко обнаружить с помощью программы контекстного поиска, похожей на ту, которую мы рассматривали в . Функция ferror выдает ненулевое значение, если в файле fp была обнаружена ошибка.

int ferror(FILE *fp)

Хотя при выводе редко возникают ошибки, все же они встречаются (например, оказался переполненным диск); поэтому в программах широкого пользования они должны тщательно контролироваться.

Функция feof(FILE *fp) aнaлoгичнa функции ferror; oнa вoзвpaщaeт нeнулевое значение, если встретился конец указанного в аргументе файла.

int feof(FILE *fp);

В наших небольших иллюстративных программах мы не заботились о выдаче статуса выхода, т. е. выдаче некоторого числа, характеризующего состояние программы в момент завершения: работа закончилась нормально или прервана из-за ошибки? Если работа прервана в результате ошибки, то какой? Любая серьезная программа должна выдавать статус выхода.


Управление памятью


Функции malloc и calloc динамически запрашивают блоки свободной памяти. Функция malloc

void *malloc(size_t n)

возвращает указатель на n байт неинициализированной памяти или NULL, если запрос удовлетворить нельзя. Функция calloc

void *calloc(size_t n, size_t size)

возвращает указатель на область, достаточную для хранения массива из n объектов указанного размера (size), или NULL, если запрос не удается удовлетворить. Выделенная память обнуляется.

Указатель, возвращаемый функциями malloc и calloc, будет выдан с учетом выравнивания, выполненного согласно указанному типу объекта. Тем не менее к нему должна быть применена операция приведения к соответствующему типу (Как уже отмечалось (см. примеч. в ), замечания о приведении типов значений, возвращаемых функциями malloc или calloc, — неверно. — Примеч. авт.), как это сделано в следующем фрагменте программы:

int *ip; ip = (int *) calloc(n, sizeof(int));

Функция free(p) освобождает область памяти, на которую указывает p, - указатель, первоначально полученный с помощью malloc или calloc. Никаких ограничений на порядок, в котором будет освобождаться память, нет, но считается ужасной ошибкой освобождение тех областей, которые не были получены с помощью calloc или malloc.

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

for (p = head; p != NULL; p = p->next) /* НЕВЕРНО */ free(p);

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

for (p = head; p != NULL; p = q) { q = p->next; free(p); }

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



Условная компиляция


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

Вычисляется константное целое выражение, заданное в строке #if. Это выражение не должно содержать ни одного оператора sizeof или приведения к типу и ни одной enum-константы. Если оно имеет ненулевое значение, то будут включены все последующие строки вплоть до #endif, или #elif, или #else. (Инструкция препроцессора #elif похожа на else if.) Выражение defined(имя) в #if есть 1, если имя было определено, и 0 в противном случае.

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

#if !defined(HDR) #define HDR

/* здесь содержимое hdr.h */

#endif

При первом включении файла hdr.h будет определено имя HDR, а при последующих включениях препроцессор обнаружит, что имя HDR уже определено, и перескочит сразу на #endif. Этот прием может оказаться полезным, когда нужно избежать многократного включения одного и того же файла. Если им пользоваться систематически, то в результате каждый заголовочный файл будет сам включать заголовочные файлы, от которых он зависит, освободив от этого занятия пользователя.

Вот пример цепочки проверок имени SYSTEM, позволяющей выбрать нужный файл для включения:

#if SYSTEM == SYSV #define HDR "sysv.h" #elif SYSTEM == BSD #define HDR "bsd.h" #elif SYSTEM == MSDOS #define HDR "msdos.h" #else #define HDR "default.h" #endif #include HDR

Инструкции #ifdef и #ifndef специально предназначены для проверки того, определено или нет заданное в них имя. И следовательно, первый пример, приведенный выше для иллюстрации #if, можно записать и в таком виде:

#ifndef HDR #define HDR

/* здесь содержимое hdr.h */

#endif



Условные выражения


Инструкции

if (a > b) z = a; else z = b;

пересылают в z большее из двух значений a и b. Условное выражение, написанное с помощью тернарного (т. е. имеющего три операнда) оператора "? : ", представляет собой другой способ записи этой и подобных ей конструкций. В выражении

выр1 ? выр2 : выр3

первым вычисляется выражение выр1. Если его значение не нуль (истина), то вычисляется выражение выр2, и значение этого выражения становится значением всего условного выражения. В противном случае вычисляется выражение выр3 и его значение становится значением условного выражения. Следует отметить, что из выражений выр2 и выр3 вычисляется только одно из них. Таким образом, чтобы установить в z большее из a и b, можно написать

z = (a > b) ? a : b; /* z = max(a, b) */

Следует заметить, что условное выражение и в самом деле является выражением, и его можно использовать в любом месте, где допускается выражение. Если выр2 и выр3 принадлежат разным типам, то тип результата определяется правилами преобразования, о которых шла речь в этой главе ранее. Например, если f имеет тип float, а n - тип int, то типом выражения

(n > 0) ? f : n

будет float вне зависимости от того, положительно значение n или нет.

Заключать в скобки первое выражение в условном выражении не обязательно, так как приоритет ?: очень низкий (более низкий приоритет имеет только присваивание), однако мы рекомендуем всегда это делать, поскольку благодаря обрамляющим скобкам условие в выражении лучше воспринимается.

Условное выражение часто позволяет сократить программу. В качестве примера приведем цикл, обеспечивающий печать n элементов массива по 10 на каждой строке с одним про6елом между колонками; каждая строка цикла, включая последнюю, заканчивается символом новой строки:

for (i = 0; i < n; i++) printf("%6d %c", a[i], (i%10 == 9 i == n-1) ? '\n' : ' ');

Символ новой строки посылается после каждого десятого и после n-го элемента. За всеми другими элементами следует пробел. Эта программа выглядит довольно замысловато, зато она более компактна, чем эквивалентная программа с использованием if-else. Вот еще один хороший пример :

printf("Вы имеете %d элемент%s: \n", n, (n%10 == 1 && n%100 != 11) ? " " : ((n%100 < 10 n%100 > 20) && n%10 >= 2 && n%10

Упражнение 2.10. Напишите функцию lower, которая переводит большие буквы в малые, используя условное выражение (а не конструкцию if-else).



Включение файла


Средство #include позволяет, в частности, легко манипулировать наборами #define и объявлений. Любая строка вида

#include "имя-файла"

или

#include имя-файла>

заменяется содержимым файла с именем имя-файла. Если имя-файла заключено в двойные кавычки, то, как правило, файл ищется среди исходных файлов программы; если такового не оказалось или имя-файла заключено в угловые скобки < и >, то поиск осуществляется по определенным в реализации правилам. Включаемый файл сам может содержать в себе строки #include.

Часто исходные файлы начинаются с нескольких строк #include, ссылающихся на общие инструкции #define и объявления extern или прототипы нужных библиотечных функций из заголовочных файлов вроде <stdio.h>. (Строго говоря, эти включения не обязательно являются файлами; технические детали того, как осуществляется доступ к заголовкам, зависят от конкретной реализации.)

Средство #include - хороший способ собрать вместе объявления большой программы. Он гарантирует, что все исходные файлы будут пользоваться одними и теми же определениями и объявлениями переменных, благодаря чему предотвращаются особенно неприятные ошибки. Естественно, при внесении изменений во включаемый файл все зависимые от него файлы должны перекомпилироваться.



Внешние переменные


Программа на Си обычно оперирует с множеством внешних объектов: переменных и функций. Прилагательное "внешний" (external) противоположно прилагательному "внутренний", которое относится к аргументам и переменным, определяемым внутри функций. Внешние переменные определяются вне функций и потенциально доступны для многих функций. Сами функции всегда являются внешними объектами, поскольку в Си запрещено определять функции внутри других функций. По умолчанию одинаковые внешние имена, используемые в разных файлах, относятся к одному и тому же внешнему объекту (функции). (В стандарте это называется редактированием внешних связей (линкованием) (external linkage).) В этом смысле внешние переменные похожи на области COMMON в Фортране и на переменные самого внешнего блока в Паскале. Позже мы покажем, как внешние функции и переменные сделать видимыми только внутри одного исходного файла.

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

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

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


В связи с приведенными рассуждениями разберем пример. Поставим себе задачу написать программу-калькулятор, понимающую операторы +, -, * и /. Такой калькулятор легче будет написать, если ориентироваться на польскую, а не инфиксную запись выражений. (Обратная польская запись применяется в некоторых карманных калькуляторах и в таких языках, как Forth и Postscript.) В обратной польской записи каждый оператор следует за своими операндами. Выражение в инфиксной записи, скажем

(1 - 2) * (4 + 5)

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

1 2 – 4 5 + *

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

Реализовать нашу программу весьма просто. Каждый операнд посылается в стек; если встречается оператор, то из стека берется соответствующее число операндов (в случае бинарных операторов два) и выполняется операция, после чего результат посылается в стек. В нашем примере числа 1 и 2 посылаются в стек, затем замещаются на их разность -1. Далее в стек посылаются числа 4 и 5, которые затем заменяются их суммой (9). Числа -1 и 9 заменяются в стеке их произведением (т. е. -9). Встретив символ новой строки, программа извлекает значение из стека и печатает его.

Таким образом, программа состоит из цикла, обрабатывающего на каждом своем шаге очередной встречаемый оператор или операнд:

while (следующий элемент не конец-файла) if (число) послать его в стек

else if (оператор) взять из стека операнды выполнить операцию результат послать в стек

else if (новая-строка) взять с вершины стека число и напечатать

else ошибка

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



Главный вопрос, который мы еще не рассмотрели, - это вопрос о том, где расположить стек и каким функциям разрешить к нему прямой доступ. Стек можно расположить в функции main и передавать сам стек и текущую позицию в нем в качестве аргументов функциям push ("послать в стек") и pop ("взять из стека"). Но функции main нет дела до переменных, относящихся к стеку, - ей нужны только операции по помещению чисел в стек и извлечению их оттуда. Поэтому мы решили стек и связанную с ним информацию хранить во внешних переменных, доступных для функций push и pop, но не доступных для main.

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

#include /* могут быть в любом количестве */ #define /* могут быть в любом количестве */

объявления функций для main

main() {...} внешние переменные для push и pop

void push (double f) {...} double pop (void) {...}

int getop(char s[]) {...}

подпрограммы, вызываемые функцией getop

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

Функция main - это цикл, содержащий большой переключатель switch, передающий управление на ту или иную ветвь в зависимости от типа оператора или операнда. Здесь представлен более типичный случай применения переключателя switch по сравнению с рассмотренным в .

#include <stdio.h> #include <stdlib.h> /* для atof() */

#define MAXOP 100 /* макс. размер операнда или оператора */ #define NUMBER '0' /* признак числа */

int getop (char []); void push (double); double pop (void);

/* калькулятор с обратной польской записью */ main() { int type; double op2; char s[MAXOP];

while ((type = getop (s)) != EOF) { switch (type) { case NUMBER: push (atof(s)); break; case '+': push (pop() + pop()); break; case '*': push (pop() * pop()); break; case '-': op2 = pop(); push (pop() - op2); break; case '/': pop2 = pop(); if (op2 != 0.0) push (pop() / op2); else printf("ошибка: деление на нуль\n"); break; case '\n': printf("\t%.8g\n", pop()); break; default: printf("ошибка: неизвестная операция %s\n", s); break; } } return 0; }



Так как операторы + и * коммутативны, порядок, в котором операнды берутся из стека, не важен, однако в случае операторов - и /, левый и правый операнды должны различаться. Так, в

push(pop() – pop()); /* НЕПРАВИЛЬНО */

очередность обращения к pop не определена. Чтобы гарантировать правильную очередность, необходимо первое значение из стека присвоить временной переменной, как это и сделано в main.

#define MAXVAL 100 /* максимальная глубина стека */

int sp = 0; /* следующая свободная позиция в стеке */ double val[MAXVAL]; /* стек */

/* push: положить значение f в стек */ void push(double f) { if (sp < MAXVAL) val[sp++] = f; else printf("ошибка: стек полон, %g не помещается\n", f); }

/* pop: взять с вершины стека и выдать в качестве результата */ double pop(void) { if (sp > 0) return val[--sp]; else { printf ("ошибка: стек пуст\n"); return 0.0; } }

Переменная считается внешней, если она определена вне функции. Таким образом, стек и индекс стека, которые должны быть доступны и для push, и для pop, определяются вне этих функций. Но main не использует ни стек, ни позицию в стеке, и поэтому их представление может быть скрыто от main.

Займемся реализацией getop - функции, получающей следующий оператор или операнд. Нам предстоит решить довольно простую задачу. Более точно: требуется пропустить пробелы и табуляции; если следующий символ - не цифра и не десятичная точка, то нужно выдать его; в противном случае надо накопить строку цифр с десятичной точкой, если она есть, и выдать число NUMBER в качестве результата.

#include <ctype.h>

int getch(void); void ungetch(int);

/* getop: получает следующий оператор или операнд */ int getop(char s[]) { int i, с; while ((s[0] = с = getch()) == ' ' с == '\t') ; s[1] = '\0; if (!isdigit(c) && с != '.') return c; /* не число */ i = 0; if (isdigit(c)) /* накапливаем целую часть */ while (isdigit(s[++i] == с = getch())) ; if (с =='.') /* накапливаем дробную часть */ while (isdigit(s[++i] = с = getch())) ; s[i] = '\0'; if (c != EOF) ungetch(c); return NUMBER; }



Как работают функции getch и ungetch? Во многих случаях программа не может "сообразить", прочла ли она все, что требуется, пока не прочтет лишнего. Так, накопление числа производится до тех пор, пока не встретится символ, отличный от цифры. Но это означает, что программа прочла на один символ больше, чем нужно, и последний символ нельзя включать в число.

Эту проблему можно было бы решить при наличии обратной чтению операции "положить-назад", с помощью которой можно было бы вернуть ненужный символ. Тогда каждый раз, когда программа считает на один символ больше, чем требуется, эта операция возвращала бы его вводу, и остальная часть программы могла бы вести себя так, будто этот символ вовсе и не читался. К счастью, описанный механизм обратной посылки символа легко моделируется с помощью пары согласованных друг с другом функций, из которых getch поставляет очередной символ из ввода, a ungetch отправляет символ назад во входной поток, так что при следующем обращении к getch мы вновь его получим.

Нетрудно догадаться, как они работают вместе. Функция ungetch запоминает посылаемый назад символ в некотором буфере, представляющем собой массив символов, доступный для обеих этих функций; getch читает из буфера, если там что-то есть, или обращается к getchar, если буфер пустой. Следует предусмотреть индекс, указывающий на положение текущего символа в буфере.

Так как функции getch и ungetch совместно используют буфер и индекс, значения последних должны между вызовами сохраняться. Поэтому буфер и индекс должны быть внешними по отношению к этим программам, и мы можем записать getch, ungetch и общие для них переменные в следующем виде:

#define BUFSIZE 100

char buf[BUFSIZE]; /* буфер для ungetch */ int bufp = 0; /* след. свободная позиция в буфере */

int getch(void) /* взять (возможно возвращенный) символ */ { return (bufp > 0) ? buf[--bufp] : getchar(); }

void ungetch(int c) /* вернуть символ на ввод */ { if (bufp >= BUFSIZE) printf("ungetch: слишком много символов\n"); else buf[bufp++] = с; }



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

Упражнение 4.3. Исходя из предложенной нами схемы, дополните программу- калькулятор таким образом, чтобы она "понимала" оператор получения остатка от деления (%) и отрицательные числа.

Упражнение 4.4. Добавьте команды, с помощью которых можно было бы печатать верхний элемент стека (с сохранением его в стеке), дублировать его в стеке, менять местами два верхних элемента стека. Введите команду очистки стека.

Упражнение 4.5. Предусмотрите возможность использования в программе библиотечных функций sin, ехр и pow. См. библиотеку <math.h> в приложении B ().

Упражнение 4.6. Введите команды для работы с переменными (легко обеспечить до 26 переменных, каждая из которых имеет имя, представленное одной буквой латинского алфавита). Добавьте переменную, предназначенную для хранения самого последнего из напечатанных значений.

Упражнение 4.7. Напишите программу ungets(s), возвращающую строку s во входной поток. Должна ли ungets "знать" что-либо о переменных buf и bufp, или ей достаточно пользоваться только функцией ungetch?

Упражнение 4.8. Предположим, что число символов, возвращаемых назад, не превышает 1. Модифицируйте с учетом этого факта функции getch и ungetch.

Упражнение 4.9. В наших функциях не предусмотрена возможность возврата EOF. Подумайте, что надо сделать, чтобы можно было возвращать EOF, и скорректируйте соответственно программу.

Упражнение 4.10. В основу программы калькулятора можно положить применение функции getline, которая читает целиком строку; при этом отпадает необходимость в getch и ungetch. Напишите программу, реализующую этот подход.


Внешние переменные и область видимости


Переменные line, longest и прочие принадлежат только функции main, или, как говорят, локальны в ней. Поскольку они объявлены внутри main, никакие другие функции прямо к ним обращаться не могут. То же верно и применительно к переменным других функций. Например, i в getline не имеет никакого отношения к i в copy. Каждая локальная переменная функции возникает только в момент обращения к этой функции и исчезает после выхода из нее. Вот почему такие переменные, следуя терминологии других языков, называют автоматическими. (В обсуждается класс памяти static, который позволяет локальным переменным сохранять свои значения в промежутках между вызовами.)

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

В качестве альтернативы автоматическим переменным можно определить внешние переменные, к которым разрешается обращаться по их именам из любой функции. (Этот механизм аналогичен области COMMON в Фортране и определениям переменных в самом внешнем блоке в Паскале.) Так как внешние переменные доступны повсеместно, их можно использовать вместо аргументов для связи между функциями по данным. Кроме того, поскольку внешние переменные существуют постоянно, а не возникают и исчезают на период выполнения функции, свои значения они сохраняют и после возврата из функций, их установивших.

Внешняя переменная должна быть определена, причем только один раз, вне текста любой функции; в этом случае ей будет выделена память. Она должна быть объявлена во всех функциях, которые хотят ею пользоваться. Объявление содержит сведения о типе переменной. Объявление может быть явным, в виде инструкции extern, или неявным, когда нужная информация получается из контекста. Чтобы конкретизировать сказанное, перепишем программу печати самой длинной строки с использованием line, longest и max в качестве внешних переменных. Это потребует изменений в вызовах, объявлениях и телах всех трех функций.

#include <stdio.h>


#define MAXLINE 1000 /* максимальный размер вводимой строки */

int max; /* длина максимальной из просмотренных строк */ char line[MAXLINE]; /* текущая строка */ char longest[MAXLINE]; /* самая длинная строка */

int getline(void); void copy(void);

/* печать самой длинной строки: специализированная версия */ main() { int len; extern int max; extern char longest[]; max = 0; while ((len = getline()) > 0) if (len > max) { max = len; copy(); } if (max > 0) /* была хотя бы одна строка */ printf(“%s”, longest); return 0; }

/* getline: специализированная версия */ int getline(void) { int c, i; extern char line[]; for (i = 0; i < MAXLINE-1 && (c=getchar()) != EOF && c != '\n'; ++i) line[i] = c; if(c == '\n') { line[i]= c; ++i; } line[i] = '\0'; return i; }

/* copy: специализированная версия */ void copy(void) { int i; extern char line[], longest[];

i = 0; while ((longest[i] = line[i]) != '\0') ++i; }

Внешние переменные для main, getline и copy определяются в начале нашего примера, где им присваивается тип и выделяется память. Определения внешних переменных синтаксически ничем не отличаются от определения локальных переменных, но поскольку они расположены вне функций, эти переменные считаются внешними. Чтобы функция могла пользоваться внешней переменной, ей нужно прежде всего сообщить имя соответствующей переменной. Это можно сделать, например, задав объявление extern, которое по виду отличается от объявления внешней переменной только тем, что оно начинается с ключевого слова extern.

В некоторых случаях объявление extern можно опустить. Если определение внешней переменной в исходном файле расположено выше функции, где она используется, то в объявлении extern нет необходимости. Таким образом, в main, getline и copy объявления extern избыточны. Обычно определения внешних переменных располагают в начале исходного файла, и все объявления extern для них опускают.

Если же программа расположена в нескольких исходных файлах и внешняя переменная определена в файле1, а используется в файле2 и файлеЗ, то объявления extern в файле2 и файлеЗ обязательны, поскольку необходимо указать, что во всех трех файлах функции обращаются к одной и той же внешней переменной. На практике обычно удобно собрать все объявления внешних переменных и функций в отдельный файл, называемый заголовочным (header



-файлом), и помещать его с помощью #include в начало каждого исходного файла. В именах header-файлов по общей договоренности используется суффикс .h. В этих файлах, в частности в <stdio.h>, описываются также функции стандартной библиотеки. Более подробно о заголовочных файлах говорится в , а применительно к стандартной библиотеке - в и .

Так как специализированные версии getline и copy не имеют аргументов, на первый взгляд кажется, что логично их прототипы задать в виде getline() и copy(). Но из соображений совместимости со старыми Си-программами стандарт рассматривает пустой список как сигнал к тому, чтобы выключить все проверки на соответствие аргументов. Поэтому, когда нужно сохранить контроль и явно указать отсутствие аргументов, следует пользоваться словом void. Мы вернемся к этой проблеме в .

Заметим, что по отношению к внешним переменным в этом параграфе мы очень аккуратно используем понятия определение и объявление. “Определение” располагается в месте, где переменная создается и ей отводится память; “объявление” помещается там, где фиксируется природа переменной, но никакой памяти для нее не отводится.

Следует отметить тенденцию все переменные делать внешними. Дело в том, что, как может показаться на первый взгляд, это приводит к упрощению связей - ведь списки аргументов становятся короче, а переменные доступны везде, где они нужны; однако они оказываются доступными и там, где не нужны. Так что чрезмерный упор на внешние переменные чреват большими опасностями - он приводит к созданию программ, в которых связи по данным не очевидны, поскольку переменные могут неожиданным и даже таинственным способом изменяться. Кроме того, такая программа с трудом поддается модификациям. Вторая версия программы поиска самой длинной строки хуже, чем первая, отчасти по этим причинам, а отчасти из-за нарушения общности двух полезных функций, вызванного тем, что в них вписаны имена конкретных переменных, с которыми они оперируют.

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



Упражнение 1.20. Напишите программу detab, заменяющую символы табуляции во вводимом тексте нужным числом пробелов (до следующего “стопа” табуляции). Предполагается, что “стопы” табуляции расставлены на фиксированном расстоянии друг от друга, скажем, через n позиций. Как лучше задавать n - в виде значения переменной или в виде именованной константы?

Упражнение 1.21. Напишите программу entab, заменяющую строки из пробелов минимальным числом табуляций и пробелов таким образом, чтобы вид напечатанного текста не изменился. Используйте те же “стопы” табуляции, что и в detab. В случае, когда для выхода на очередной “стоп” годится один пробел, что лучше - пробел или табуляция?

Упражнение 1.22. Напишите программу, печатающую символы входного потока так, чтобы строки текста не выходили правее n-й позиции. Это значит, что каждая строка, длина которой превышает n, должна печататься с переносом на следующие строки. Место переноса следует “ искать” после последнего символа, отличного от символа- разделителя, расположенного левее n-й позиции. Позаботьтесь о том, чтобы ваша программа вела себя разумно в случае очень длинных строк, а также когда до n-й позиции не встречается ни одного символа пробела или табуляции.

Упражнение 1.23. Напишите программу, убирающую все комментарии из любой Си- программы. Не забудьте должным образом обработать строки символов и строковые константы. Комментарии в Си не могут быть вложены друг в друга.

Упражнение 1.24. Напишите программу, проверяющую Си-программы на элементарные синтаксические ошибки вроде несбалансированности скобок всех видов. Не забудьте о кавычках (одиночных и двойных), эскейп-последовательностях (\...) и комментариях. (Это сложная программа, если писать ее для общего случая.)


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


Си - универсальный язык программирования. Он тесно связан с системой UNIX, так как был разработан в этой системе, которая как и большинство программ, работающих в ней, написаны на Си. Однако язык не привязан жестко к какой-то одной операционной системе или машине. Хотя он и назван "языком системного программирования", поскольку удобен для написания компиляторов и операционных систем, оказалось, что на нем столь же хорошо писать большие программы другого профиля.
Многие важные идеи Си взяты из языка BCPL, автором которого является Мартин Ричардс. Влияние BCPL на Си было косвенным - через язык B, разработанный Кеном Томпсоном в 1970 г. для первой системы UNIX, реализованной на PDP-7.
BCPL и B - "бестиповые" языки. В отличие от них Си обеспечивает разнообразие типов данных. Базовыми типами являются символы, а также целые и числа с плавающей точкой различных размеров. Кроме того, имеется возможность получать целую иерархию производных типов данных из указателей, массивов, структур и объединений. Выражения формируются из операторов и операндов. Любое выражение, включая присваивание и вызов функции, может быть инструкцией. Указатели обеспечивают машинно-независимую адресную арифметику.
В Си имеются основные управляющие конструкции, используемые в хорошо структурированных программах: составная инструкция ({. . .}), ветвление по условию (if-else), выбор одной альтернативы из многих (switch), циклы с проверкой наверху (while, for) и с проверкой внизу (do), а также средство прерывания цикла (break).
В качестве результата функции могут возвращать значения базовых типов, структур, объединений и указателей. Любая функция допускает рекурсивное обращение к себе. Как правило, локальные переменные функции - "автоматические", т. е. они создаются заново при каждом обращении к ней. Определения функций нельзя вкладывать друг в друга, но объявления переменных разрешается строить в блочно-структурной манере. Функции программы на Си могут храниться в отдельных исходных файлах и компилироваться независимо. Переменные по отношению к функции могут быть внутренними и внешними. Последние могут быть доступными в пределах одного исходного файла или всей программы.


На этапе препроцессирования выполняется макроподстановка в текст программы, включение других исходных файлов и условная компиляция.
Си - язык сравнительно "низкого уровня". Однако это вовсе не умаляет его достоинств, просто Си имеет дело с теми же объектами, что и большинство компьютеров, т. е. с символами, числами и адресами. С ними можно оперировать при помощи арифметических и логических операций, выполняемых реальными машинами.
В Си нет прямых операций над составными объектами, такими как строки символов, множества, списки и массивы. В нем нет операций, которые бы манипулировали с целыми массивами или строками символов, хотя структуры разрешается копировать целиком как единые объекты. В языке нет каких-либо средств распределения памяти, помимо возможности определения статических переменных и стекового механизма при выделении места для локальных переменных внутри функций. Нет в нем "кучи" и "сборщика мусора". Наконец, в самом Си нет средств ввода-вывода, инструкций READ (читать) и WRITE (писать) и каких-либо методов доступа к файлам. Все это - механизмы высокого уровня, которые в Си обеспечиваются исключительно с помощью явно вызываемых функций. Большинство реализованных Си-систем содержат в себе разумный стандартный набор этих функций.
В продолжение сказанного следует отметить, что Си предоставляет средства лишь последовательного управления ходом вычислений: механизм ветвления по условиям, циклы, составные инструкции, подпрограммы и не содержит средств мультипрограммирования, параллельных процессов, синхронизации и организации сопрограмм.
Отсутствие некоторых из перечисленных средств может показаться серьезным недостатком ("выходит, чтобы сравнить две строки символов, нужно обращаться к функции?"). Однако компактность языка имеет реальные выгоды. Поскольку Си относительно мал, то и описание его кратко, и овладеть им можно быстро. Программист может реально рассчитывать на то, что он будет знать, понимать и на практике регулярно пользоваться всеми возможностями языка.


В течение многих лет единственным определением языка Си было первое издание книги "Язык программирования Си". В 1983 г. Институтом американских национальных стандартов (ANSI) учреждается комитет для выработки современного исчерпывающего определения языка Си. Результатом его работы явился стандарт для Си ("ANSI-C"), выпущенный в 1988 г. Большинство положений этого стандарта уже учтено в современных компиляторах.
Стандарт базируется на первоначальном справочном руководстве. По сравнению с последним язык изменился относительно мало. Одной из целей стандарта было обеспечить, чтобы в большинстве случаев существующие программы оставались правильными или вызывали предупреждающие сообщения компиляторов об изменении поведения.
Для большинства программистов самое важное изменение - это новый синтаксис объявления и определения функций. Объявление функции может теперь включать и описание ее аргументов. В соответствии с этим изменился и синтаксис определения функции. Дополнительная информация значительно облегчает компилятору выявление ошибок, связанных с несогласованностью аргументов; по нашему мнению, это очень полезное добавление к языку.
Следует также отметить ряд небольших изменений. В языке узаконены присваивание структур и перечисления, которые уже некоторое время широко используются. Вычисления с плавающей точкой теперь допускаются и с одинарной точностью. Уточнены свойства арифметики, особенно для беззнаковых типов. Усовершенствован препроцессор. Большинство программистов эти изменения затронут очень слабо.
Второй значительный вклад стандарта - это определение библиотеки, поставляемой вместе с Си- компилятором, в которой специфицируются функции доступа к возможностям операционной системы (например чтения-записи файлов), форматного ввода-вывода, динамического выделения памяти, манипуляций со строками символов и т. д. Набор стандартных заголовочных файлов обеспечивает единообразный доступ к объявлениям функций и типов данных. Гарантируется, что программы, использующие эту библиотеку при взаимодействии с операционной системой, будут работать также и на других машинах. Большинство программ, составляющих библиотеку, созданы по образу и подобию "стандартной библиотеки ввода-вывода" системы UNIX. Эта библиотека описана в первом издании книги и широко используется в других системах. И здесь программисты не заметят существенных различий. Так как типы данных и управляющих структур языка Си поддерживаются командами большинства существующих машин, исполнительная система (run-time library), обеспечивающая независимый запуск и выполнение программ, очень мала. Обращения к библиотечным функциям пишет сам программист (не компилятор), поэтому при желании их можно легко заменить на другие. Почти все программы, написанные на Си, если они не касаются каких-либо скрытых в операционной системе деталей, переносимы на другие машины.


Си соответствует аппаратным возможностям многих машин, однако он не привязан к архитектуре какой- либо конкретной машины. Проявляя некоторую дисциплину, можно легко писать переносимые программы, т. е. программы, которые без каких-либо изменений могут работать на разных машинах. Стандарт предоставляет возможность для явного описания переносимости с помощью набора констант, отражающих характеристики машины, на которой программа будет работать.
Си не является "строго типизированным" языком, но в процессе его развития контроль за типами был усилен. В первой версии Си хоть не одобрялся, но разрешался бесконтрольный обмен указателей и целых, что вызывало большие нарекания, но это уже давным-давно запрещено. Согласно стандарту теперь требуется явное объявление или явное указание преобразования, что уже и реализовано в хороших компиляторах. Новый вид объявления функций - еще один шаг в этом направлении. Компилятор теперь предупреждает о большей части ошибок в типах и автоматически не выполняет преобразования данных несовместимых типов. Однако основной философией Си остается то, что программисты сами знают, что делают; язык лишь требует явного указания об их намерениях.
Си, как и любой другой язык программирования, не свободен от недостатков. Уровень старшинства некоторых операторов не является общепринятым, некоторые синтаксические конструкции могли бы быть лучше. Тем не менее, как оказалось, Си - чрезвычайно эффективный и выразительный язык, пригодный для широкого класса задач.
Книга имеет следующую структуру. представляет собой обзор основных средств языка Си. Ее назначение - побудить читателя по возможности быстрее приступить к программированию, так как мы убеждены, что единственный способ изучить новый язык - это писать на нем программы. Эта часть книги предполагает наличие знаний по основным элементам программирования. Никаких пояснений того, что такое компьютер, компиляция или что означает выражение вида n=n+1 не дается. Хотя мы и пытались там, где это возможно, показать полезные приемы программирования, эта книга не призвана быть справочником ни по работе со структурами данных, ни по алгоритмам: когда оказывалось необходимым выбрать, на что сделать ударение, мы предпочитали сконцентрировать внимание на языке.


В главах 2- 6 различные средства языка обсуждаются более подробно и несколько более формально, чем в главе 1; при этом по-прежнему упор делается на примеры, являющиеся законченными программами, а не изолированными фрагментами. знакомит с базовыми типами данных, с операторами и выражениями. В рассматриваются средства управления последовательностью вычислений: if-else, switch, while, for и т.д. В речь идет о функциях и структуре программы (внешних переменных, правилах видимости, делении программы на несколько исходных файлов и т. д.), а также о препроцессоре. В обсуждаются указатели и адресная арифметика. посвящена структурам и объединениям.
В описана стандартная библиотека, обеспечивающая общий интерфейс с операционной системой. Эта библиотека узаконена в качестве стандарта ANSI, иначе говоря, она должна быть представлена на всех машинах, где существует Си, благодаря чему программы, использующие ввод-вывод и другие возможности операционной системы, без каких-либо изменений можно переносить с одной машины на другую.
содержит описание интерфейса между программами на Си и операционной системой UNIX, в частности описание ввода-вывода, файловой системы и распределения памяти. Хотя некоторые параграфы этой главы отражают специфику системы UNIX, программисты, пользующиеся другими системами, все же найдут в них много полезных сведений, включая определенный взгляд на то, как реализуется одна из версий стандартной библиотеки, и некоторые предложения по переносимости программ.
является справочником по языку. Строгое определение синтаксиса и семантики языка Си содержится в официальном документе стандарта ANSI. Последний, однако, более всего подходит разработчикам компилятора. Наш справочник определяет язык более сжато, не прибегая к педантично юридическому стилю, которым пользуется стандарт. - сводка по содержимому стандартной библиотеки и предназначена скорее пользователям, чем реализаторам. В приводится краткий перечень отличий от первой версии языка. В сомнительных случаях, однако, окончательным судьей по языку остается стандарт и компилятор, которым вы пользуетесь.

Ввод-вывод символов


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

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

Стандартная библиотека включает несколько функций для чтения и записи одного символа. Простейшие из них - getchar

и putchar. За одно обращение к getchar считывается следующий символ ввода из текстового потока, и этот символ выдается в качестве результата. Так, после выполнения

c = getchar();

переменная c содержит очередной символ ввода. Обычно символы поступают с клавиатуры. Ввод из файлов рассматривается в .

Обращение к putchar приводит к печати одного символа. Так,

putchar(c);

напечатает содержимое целой переменной c в виде символа (обычно на экране). Вызовы putchar и printf могут произвольным образом перемежаться. Вывод будет формироваться в том же порядке, что и обращения к этим функциям.



Ввод-вывод строк


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

char *fgets(char *line, int maxline, FILE *fp)

Функция fgets читает следующую строку ввода (включая и символ новой строки) из файла fp в массив символов line, причем она может прочитать не более MAXLINE-1 символов. Переписанная строка дополняется символом '\0'. Обычно fgets возвращает line, а по исчерпании файла или в случае ошибки - NULL. (Наша getline возвращала длину строки, которой мы потом пользовались, и нуль в случае конца файла.)

Функция вывода fputs пишет строку (которая может и не заканчиваться символом новой строки) в файл.

int fputs(char *line, FILE *fp)

Эта функция возвращает EOF, если возникла ошибка, и неотрицательное значение в противном случае.

Библиотечные функции gets и puts подобны функциям fgets и fputs. Отличаются они тем, что оперируют только стандартными файлами stdin и stdout, и кроме того, gets выбрасывает последний символ '\n', a puts его добавляет.

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

/* fgets: получает не более n символов из iop */ char *fgets(char *s, int n, FILE *iop) { register int c; register char *cs;

cs = s; while (--n > 0 && (с = getc(iop)) != EOF) if ((*cs++ = c) == '\n') break; *cs= '\0'; return (c == EOF && cs == s) ? NULL : s; }

/* fputs: посылает строку s в файл iop */ int fputs(char *s, FILE *iop) { int c;

while (c = *s++) putc(c, iop); return ferror(iop) ? EOF : 0; }

Стандарт определяет, что функция ferror возвращает в случае ошибки ненулевое значение; fputs в случае ошибки возвращает EOF, в противном случае - неотрицательное значение.

С помощью fgets легко реализовать нашу функцию getline:

/* getline: читает строку, возвращает ее длину */ int getline(char *line, int max) { if (fgets(line, max, stdin) == NULL) return 0; else return strlen(line); }

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

Упражнение 7.7. Модифицируйте программу поиска по образцу из таким образом, чтобы она брала текст из множества именованных файлов, а если имен файлов в аргументах нет, то из стандартного ввода. Будет ли печататься имя файла, в котором найдена подходящая строка?

Упражнение 7.8. Напишите программу, печатающую несколько файлов. Каждый файл должен начинаться с новой страницы, предваряться заголовком и иметь свою нумерацию страниц.



Заголовочные файлы


Теперь представим себе, что компоненты программы-калькулятора имеют существенно большие размеры, и зададимся вопросом, как в этом случае распределить их по нескольким файлам. Программу main поместим в файл, который мы назовем main.с; push, pop и их переменные расположим во втором файле, stack.с; a getop - в третьем, getop.c. Наконец, getch и ungetch разместим в четвертом файле getch.с; мы отделили их от остальных функций, поскольку в реальной программе они будут получены из заранее скомпилированной библиотеки.

Существует еще один момент, о котором следует предупредить читателя, - определения и объявления совместно используются несколькими файлами. Мы бы хотели, насколько это возможно, централизовать эти объявления и определения так, чтобы для них существовала только одна копия. Тогда программу в процессе ее развития будет легче и исправлять, и поддерживать в нужном состоянии. Для этого общую информацию расположим в заголовочном файле calc.h, который будем по мере необходимости включать в другие файлы. (Строка #include описывается в ) В результате получим программу, файловая структура которой показана ниже:

main.с:

#include <stdio.h> #include <stdlib.h> #include "calc.h" #define MAXOP 100 main() { ... }

calc.h:

#define NUMBER '0' void push(double); double pop(void); int getop(char[]); int getch(void); void ungetch(int);

getop.c:

#include <stdio.h> #include <ctype.h> #include "calc.h" getop (){ ... }

getch.c:

#include <stdio.h> #define BUFSIZE 100 char buf[BUFSIZE]; intbufp = 0; int getch(void) { ... } void ungetch(int) { ... }

stack.с:

#include <stdio.h> #include "calc.h" #define MAXVAL 100 int sp = 0; double val[MAXVAL]; void push(double) { ... } double pop(void) { ... }

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