- •Б. Керниган, д. Ритчи
- •Язык программирования Си
- •Издание 3-е, исправленное
- •Предисловие
- •Предисловие к первому изданию
- •Введение
- •Глава 1. Обзор языка
- •1.1 Начнем, пожалуй
- •1.2 Переменные и арифметические выражения
- •1.3 Инструкция for
- •1.4 Именованные константы
- •1.5 Ввод-вывод символов
- •1.5.1 Копирование файла
- •1.5.2 Подсчет символов
- •1.5.3 Подсчет строк
- •1.5.4 Подсчет слов
- •1.6 Массивы
- •1.7 Функции
- •1.8 Аргументы. Вызов по значению
- •1.9 Символьные массивы
- •1.10 Внешние переменные и область видимости
- •Глава 2. Типы, операторы и выражения
- •2.1 Имена переменных
- •2.2 Типы и размеры данных
- •2.3 Константы
- •2.4 Объявления
- •2.5 Арифметические операторы
- •2.6 Операторы отношения и логические операторы
- •2.7 Преобразования типов
- •2.8 Операторы инкремента и декремента
- •2.9 Побитовые операторы
- •2.10 Операторы и выражения присваивания
- •2.11 Условные выражения
- •2.12 Приоритет и очередность вычислений
- •Глава 3. Управление
- •3.1 Инструкции и блоки
- •3.2 Конструкция if-else
- •3.3 Конструкция else-if
- •3.4 Переключатель switch
- •3.5 Циклы while и for
- •3.6 Цикл do-while
- •3.7 Инструкции break и continue
- •3.8 Инструкция goto и метки
- •Глава 4. Функции и структура программы
- •4.1 Основные сведения о функциях
- •4.2 Функции, возвращающие нецелые значения
- •4.3 Внешние переменные
- •4.4 Области видимости
- •4.5 Заголовочные файлы
- •4.6 Статические переменные
- •4.7 Регистровые переменные
- •4.8 Блочная структура
- •4.9 Инициализация
- •4.10 Рекурсия
- •4.11 Препроцессор языка Си
- •4.11.1 Включение файла
- •4.11.2 Макроподстановка
- •4.11.3 Условная компиляция
- •Глава 5. Указатели и массивы
- •5.1 Указатели и адреса
- •5.2 Указатели и аргументы функций
- •5.3 Указатели и массивы
- •5.4 Адресная арифметика
- •5.5 Символьные указатели функции
- •5.6 Массивы указателей, указатели на указатели
- •5.7 Многомерные массивы
- •5.8 Инициализация массивов указателей
- •5.9 Указатели против многомерных массивов
- •5.10 Аргументы командной строки
- •5.11 Указатели на функции
- •5.12 Сложные объявления
- •Глава 6. Структуры
- •6.1 Основные сведения о структурах
- •6.2 Структуры и функции
- •6.3 Массивы структур
- •6.4 Указатели на структуры
- •6.5 Структуры со ссылками на себя
- •6.6 Просмотр таблиц
- •6.7 Средство typedef
- •6.8 Объединения
- •6.9 Битовые поля
- •Глава 7. Ввод и вывод
- •7.1 Стандартный ввод-вывод
- •7.2 Форматный вывод (printf)
- •7.3 Списки аргументов переменной длины
- •7.4 Форматный ввод (scanf)
- •7.5 Доступ к файлам
- •7.6 Управление ошибками (stderr и exit)
- •7.7 Ввод-вывод строк
- •7.8 Другие библиотечные функции
- •7.8.1 Операции со строками
- •7.8.2 Анализ класса символов и преобразование символов
- •7.8.3 Функция ungetc
- •7.8.4 Исполнение команд операционной системы
- •7.8.5 Управление памятью
- •7.8.6 Математические функции
- •7.8.7 Генератор случайных чисел
- •Глава 8. Интерфейс с системой unix
- •8.1 Дескрипторы файлов
- •8.2 Нижний уровень ввода-вывода (read и write)
- •8.3 Системные вызовы open, creat, close, unlink
- •8.4 Произвольный доступ (lseek)
- •8.5 Пример. Реализация функций fopen и getc
- •8.6 Пример. Печать каталогов
- •8.7 Пример. Распределитель памяти
- •Приложение a. Справочное руководство a1. Введение
- •A2. Соглашения о лексике
- •A2.1. Лексемы (tokens)
- •A2.2. Комментарий
- •A2.3. Идентификаторы
- •A2.4. Ключевые слова
- •A2.5.2. Символьные константы
- •А2.5.3. Константы с плавающей точкой
- •A2.5.4. Константы-перечисления
- •A2.6. Строковые литералы
- •A3. Нотация синтаксиса
- •A4. Что обозначают идентификаторы
- •A4.1. Класс памяти
- •A4.2. Базовые типы
- •A4.3. Производные типы
- •A4.4. Квалификаторы типов
- •A5. Объекты и Lvalues
- •A6. Преобразования
- •A6.1. Целочисленное повышение
- •A6.2. Целочисленные преобразования
- •A6.3. Целые и числа с плавающей точкой
- •A6.4. Типы с плавающей точкой
- •А6.5. Арифметические преобразования
- •A6.6. Указатели и целые
- •A6.7. Тип void
- •А6.8. Указатели на void
- •A7. Выражения
- •A7.1. Генерация указателя
- •A7.2. Первичные выражения
- •A7.3. Постфиксные выражения
- •A7.3.1. Обращение к элементам массива
- •A7.3.2. Вызов функции
- •A7.3.3. Обращение к структурам
- •A7.3.4. Постфиксные операторы инкремента и декремента
- •А7.4. Унарные операторы
- •A7.4.7. Оператор логического отрицания
- •A7.4.8. Оператор определения размера sizeof
- •A7.5. Оператор приведения типа
- •A7.6. Мультипликативные операторы
- •A7.7. Аддитивные операторы
- •A7.8. Операторы сдвига
- •A7.9. Операторы отношения
- •A7.10. Операторы равенства
- •A7.11. Оператор побитового и
- •A7.12. Оператор побитового исключающего или
- •A7.13. Оператор побитового или
- •A7.14. Оператор логического и
- •A7.15. Оператор логического или
- •А7.16. Условный оператор
- •A7.17. Выражения присваивания
- •A7.18. Оператор запятая
- •A7.19. Константные выражения
- •A8. Объявления
- •A8.1. Спецификаторы класса памяти
- •А8.2. Спецификаторы типа
- •A8.3. Объявления структур и объединений
- •A8.4. Перечисления
- •А8.5. Объявители
- •A8.6. Что означают объявители
- •A8.6.1. Объявители указателей
- •А8.6.2. Объявители массивов
- •А8.6.3. Объявители функций
- •A8.7. Инициализация
- •A8.8. Имена типов
- •А8.9. Объявление typedef
- •A8.10. Эквивалентность типов
- •A9. Инструкции
- •A9.1. Помеченные инструкции
- •A9.2. Инструкция-выражение
- •A9.3. Составная инструкция
- •A9.4. Инструкции выбора
- •A9.5. Циклические инструкции
- •A9.6. Инструкции перехода
- •А10. Внешние объявления
- •A10.1. Определение функции
- •A10.2. Внешние объявления
- •A11. Область видимости и связи
- •A11.1. Лексическая область видимости
- •A11.2. Связи
- •A12. Препроцессирование
- •A12.1. Трехзнаковые последовательности
- •A12.2. Склеивание строк
- •А12.3. Макроопределение и макрорасширение
- •A12.4. Включение файла
- •A12.5. Условная компиляция
- •A12.6. Нумерация строк
- •A12.7. Генерация сообщения об ошибке
- •A12.8. Прагма
- •A12.9. Пустая директива
- •A12.10. Заранее определенные имена
- •A13. Грамматика
- •Приложение b. Стандартная библиотека
- •B1. Ввод-вывод: ‹stdio.H›
- •B1.1. Операции над файлами
- •B1.2. Форматный вывод
- •B1.3. Форматный ввод
- •B1.4. Функции ввода-вывода символов
- •B1.5. Функции прямого ввода-вывода
- •B1.6. Функции позиционирования файла
- •B1.7. Функции обработки ошибок
- •B2. Проверки класса символа: ‹ctype.H›
- •B10. Функции даты и времени: ‹time.H›
- •Приложение c. Перечень изменений
- •Предметный указатель
2.6 Операторы отношения и логические операторы
Операторами отношения являются
›
›=
‹
‹=
Все они имеют одинаковый приоритет. Сразу за ними идет приоритет операторов сравнения на равенство:
==
!=
Операторы отношения имеют более низкий приоритет, чем арифметические, поэтому выражение вроде i ‹ lim-1 будет выполняться так же, как i ‹ (lim-1), т.е. как мы и ожидаем.
Более интересны логические операторы && и || . Выражения, между которыми стоят операторы && или ||, вычисляются слева направо. Вычисление прекращается, как только становится известна истинность или ложность результата. Многие Си-программы опираются на это свойство, как, например, цикл из функции getline, которую мы приводили в главе 1:
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-циклу, не пользуясь операторами && и ||.
2.7 Преобразования типов
Если операнды оператора принадлежат к разным типам, то они приводятся к некоторому общему типу. Приведение выполняется в соответствии с небольшим числом правил. Обычно автоматически производятся лишь те преобразования, которые без какой-либо потери информации превращают операнды с меньшим диапазоном значений в операнды с большим диапазоном, как, например, преобразование целого в число с плавающей точкой в выражении вроде f+i. Выражения, не имеющие смысла, например число с плавающей точкой в роли индекса, не допускаются. Выражения, в которых могла бы теряться информация (скажем, при присваивании длинных целых переменным более коротких типов или при присваивании значений с плавающей точкой целым переменным), могут повлечь за собой предупреждение, но они допустимы.
Значения типа char - это просто малые целые, и их можно свободно использовать в арифметических выражениях, что значительно облегчает всевозможные манипуляции с символами. В качестве примера приведем простенькую реализацию функции atoi, преобразующей последовательность цифр в ее числовой эквивалент.
/* atoi: преобразование s в целое */
int atoi(char s[])
{
int i, n;
n = 0;
for (i = 0; s[i] ›= '0' && s[i] ‹= '9'; ++i)
n = 10 * n + (s[i] - '0');
return n;
}
Как мы уже говорили в главе 1, выражение
s[i] -'0'
дает числовое значение символа, хранящегося в s[i], так как значения '0', '1' и пр. образуют непрерывную возрастающую последовательность.
Другой пример приведения char к int связан с функцией lower, которая одиночный символ из набора ASCII, если он является заглавной буквой, превращает в строчную. Если же символ не является заглавной буквой, lower его не изменяет.
/* lower: преобразование c в строчную, только для ASCII */
int lower(int c)
{
if (c ›= 'A' && c ‹='Z')
return c +'a'-'A';
else
return c;
}
В случае ASCII эта программа будет работать правильно, потому что между одноименными буквами верхнего и нижнего регистров - одинаковое расстояние (если их рассматривать как числовые значения). Кроме того, латинский алфавит - плотный, т. е. между буквами A и Z расположены только буквы. Для набора EBCDIC последнее условие не выполняется, и поэтому наша программа в этом случае будет преобразовывать не только буквы.
Стандартный заголовочный файл ‹ctype.h› , описанный в приложении B, определяет семейство функций, которые позволяют проверять и преобразовывать символы независимо от символьного набора. Например, функция tolower(c) возвращает букву c в коде нижнего регистра, если она была в коде верхнего регистра, поэтому tolower - универсальная замена функции lower, рассмотренной выше. Аналогично проверку
c ›= '0' && c ‹= '9'
можно заменить на isdigit(c)
Далее мы будем пользоваться функциями из ‹ctype.h›.
Существует одна тонкость, касающаяся преобразования символов в целые числа: язык не определяет, являются ли переменные типа char знаковыми или беззнаковыми. При преобразовании char в int может ли когда- нибудь получиться отрицательное целое? На машинах с разной архитектурой ответы могут отличаться. На некоторых машинах значение типа char с единичным старшим битом будет превращено в отрицательное целое (посредством "распространения знака"). На других - преобразование char в int осуществляется добавлением нулей слева, и, таким образом, получаемое значение всегда положительно.
Гарантируется, что любой символ из стандартного набора печатаемых символов никогда не будет отрицательным числом, поэтому в выражениях такие символы всегда являются положительными операндами. Но произвольный восьмибитовый код в переменной типа char на одних машинах может быть отрицательным числом, а на других - положительным. Для совместимости переменные типа char , в которых хранятся несимвольные данные, следует специфицировать явно как signed или unsigned .
Отношения вроде i › j и логические выражения, перемежаемые операторами amp;& и || , определяют выражение-условие, которое имеет значение 1, если оно истинно, и 0, если ложно. Так, присваивание
d = c ›= '0' && c ‹= '9'
установит d в значение 1, если c есть цифра, и 0 в противном случае. Однако функции, подобные isdigit, в качестве истины могут выдавать любое ненулевое значение. В местах проверок внутри if, while, for и пр. "истина" просто означает "не нуль".
Неявные арифметические преобразования, как правило, осуществляются естественным образом. В общем случае, когда оператор вроде + или * с двумя операндами (бинарный оператор) имеет разнотипные операнды, прежде чем операция начнет выполняться, "низший" тип повышается до "высшего". Результат будет иметь высший тип. В параграфе 6 приложения 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.