
- •Предисловие
- •Предисловие к первому изданию
- •Введение
- •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. Пример. Распределитель памяти
- •А. Справочное руководство
- •А 1. Введение
- •А 2. Соглашения о лексике
- •А 2.1. Лексемы (tokens)
- •А 2.2. Комментарий
- •А 2.3. Идентификаторы
- •А 2.4. Ключевые слова
- •А 2.5. Константы
- •А 2.5.1. Целые константы
- •А 2.5.2. Символьные константы
- •А 2.5.3. Константы с плавающей точкой
- •А 2.5.4. Константы-перечисления
- •А 2.6. Строковые литералы
- •A 3. Нотация синтаксиса
- •А 4. Что обозначают идентификаторы
- •А 4.1. Класс памяти
- •А 4.2. Базовые типы
- •А 4.3. Производные типы
- •А 4.4. Квалификаторы типов
- •А 5. Объекты и Lvalues
- •А 6. Преобразования
- •А 6.1. Целочисленное повышение
- •А 6.2. Целочисленные преобразования
- •А 6.3. Целые и числа с плавающей точкой
- •А 6.4. Типы с плавающей точкой
- •А 6.5. Арифметические преобразования
- •А 6.6. Указатели и целые
- •А 6.7. Тип void
- •А 6.8. Указатели на void
- •А 7. Выражения
- •А 7.1. Генерация указателя
- •А 7.2. Первичные выражения
- •А 7.3. Постфиксные выражения
- •А 7.3.1. Обращение к элементам массива
- •А 7.3.2. Вызов функции
- •А 7.3.3. Обращение к структурам
- •А 7.3.4. Постфиксные операторы инкремента и декремента
- •А 7.4. Унарные операторы
- •А 7.4.1. Префиксные операторы инкремента и декремента
- •А 7.4.2. Оператор получения адреса
- •А 7.4.3. Оператор косвенного доступа
- •А 7.4.4. Оператор унарный плюс
- •А 7.4.5. Оператор унарный минус
- •А 7.4.6. Оператор побитового отрицания
- •А 7.4.7. Оператор логического отрицания
- •А 7.4.8. Оператор определения размера sizeof
- •А 7.5. Оператор приведения типа
- •А 7.6. Мультипликативные операторы
- •А 7.7. Аддитивные операторы
- •А 7.8. Операторы сдвига
- •А 7.9. Операторы отношения
- •А 7.10. Операторы равенства
- •А 7.11. Оператор побитового И
- •А 7.12. Оператор побитового исключающего ИЛИ
- •А 7.13. Оператор побитового ИЛИ
- •А 7.14. Оператор логического И
- •А 7.15. Оператор логического ИЛИ
- •А 7.16. Условный оператор
- •А 7.17. Выражения присваивания
- •А 7.18. Оператор запятая
- •А 7.19. Константные выражения
- •А 8. Объявления
- •А 8.1. Спецификаторы класса памяти
- •А 8.2. Спецификаторы типа
- •А 8.3. Объявления структур и объединений
- •A 8.4. Перечисления
- •А 8.6. Что означают объявители
- •А 8.6.1. Объявители указателей
- •А 8.6.2. Объявители массивов
- •А 8.6.3. Объявители функций
- •А 8.7. Инициализация
- •А 8.8. Имена типов
- •А 8.9. Объявление typedef
- •А 8.10. Эквивалентность типов
- •А 9. Инструкции
- •А 9.1. Помеченные инструкции
- •А 9.2. Инструкция-выражение
- •А 9.3. Составная инструкция
- •А 9.4. Инструкции выбора
- •А 9.5. Циклические инструкции
- •А 9.6. Инструкции перехода
- •А 10. Внешние объявления
- •А 10.1. Определение функции
- •А 10.2. Внешние объявления
- •А 11. Область видимости и связи
- •А 11.1. Лексическая область видимости
- •А 11.2. Связи
- •А 12. Препроцессирование
- •А 12.2. Склеивание строк
- •А 12.3. Макроопределение и макрорасширение
- •А 12.4. Включение файла
- •А 12.5. Условная компиляция
- •А 12.6. Нумерация строк
- •А 12.7. Генерация сообщения об ошибке
- •А 12.8. Прагма
- •А 12.9. Пустая директива
- •А 12.10. Заранее определенные имена
- •А 13. Грамматика
- •B. Стандартная библиотека
- •В 1. Ввод-вывод: <stdio.h>
- •В 1.1. Операции над файлами
- •В 1.2. Форматный вывод
- •В 1.3. Форматный ввод
- •В 1.4. Функции ввода-вывода символов
- •В 1.5. Функции прямого ввода-вывода
- •В 1.6. Функции позиционирования файла
- •В 1.7. Функции обработки ошибок
- •В 2. Проверки класса символа: <ctype.h>
- •В 3. Функции, оперирующие со строками: <string. h>
- •В 5. Функции общего назначения: <stdlib. h>
- •В 6. Диагностика: <assert. h>
- •В 7. Списки аргументов переменной длины: <stdarg.h>
- •В 8. Дальние переходы: <setjmp. h>
- •В 9. Сигналы: <signal. h>
- •В 10. Функции даты и времени: <time.h>
- •В 11. Зависящие от реализации пределы: <limits.h> и <float.h>
- •C. Перечень изменений
scanf("%d", &n);
пишут
scanf("%d", n);
Компилятор о подобной ошибке ничего не сообщает.
Упражнение 7.4. Напишите свою версию scanf по аналогии с minprintf из предыдущего параграфа.
Упражнение 7.5. Перепишите основанную на постфиксной записи программу калькулятора из главы 4 таким образом, чтобы для ввода и преобразования чисел она использовала scanf и/или sscanf.
7.5. Доступ к файлам
Во всех предыдущих примерах мы имели дело со стандартным вводом и стандартным выводом, которые для программы автоматически предопределены операционной системой конкретной машины.
Следующий шаг — научиться писать программы, которые имели бы доступ к файлам, заранее не подсоединенным к программам. Одна из программ, в которой возникает такая необходимость, — это программа cat, объединяющая несколько именованных файлов и направляющая результат в стандартный вывод. Функция cat часто применяется для выдачи файлов на экран, а также как универсальный "коллектор" файловой информации для тех программ, которые не имеют возможности обратиться к файлу по имени. Например, команда
cat x.c y.c
направит в стандартный вывод содержимое файлов x.c и y.c (и ничего более).
Возникает вопрос: что надо сделать, чтобы именованные файлы можно было читать; иначе говоря, как связать внешние имена, придуманные пользователем, с инструкциями чтения данных?
На этот счет имеются простые правила. Для того чтобы можно было читать из файла или писать в файл, он должен быть предварительно открыт с помощью библиотечной функции fopen. Функция fopen получает внешнее имя типа x.c или y.c, после чего осуществляет некоторые организационные действия и "переговоры" с операционной системой (технические детали которых здесь не рассматриваются) и возвращает указатель, используемый в дальнейшем для доступа к файлу.
Этот указатель, называемый указателем файла, ссылается на структуру, содержащую информацию о файле (адрес буфера, положение текущего символа в буфере, открыт файл на чтение или на запись, были ли ошибки при работе с файлом и не встретился ли конец файла). Пользователю не нужно знать подробности, поскольку определения, полученные из <stdio.h>, включают описание такой структуры, называемой FILE.
Единственное, что требуется для определения указателя файла, — это задать описания такого, например, вида:
FILE *fp;
FILE *fopen(char *name, char *mode);
Это говорит, что fp есть указатель на FILE, a fopen возвращает указатель на FILE. Заметим, что FILE — это имя типа, наподобие int, а не тег структуры. Оно определено с помощью typedef. (Детали того, как можно реализовать fopen в системе UNIX, приводятся в параграфе 8.5.)
Обращение к fopen в программе может выглядеть следующим образом:
fp = fopen(name, mode);
Первый аргумент — строка, содержащая имя файла. Второй аргумент несет информацию о режиме. Это тоже строка: в ней указывается, каким образом пользователь намерен применять файл. Возможны следующие режимы: чтение (read — "r"), запись (write — "w") и добавление (append — "а"), т. е. запись информации в конец уже существующего файла. В некоторых системах различаются текстовые и бинарные файлы; в случае последних в строку режима необходимо добавить букву "b" (binary — бинарный).
Тот факт, что некий файл, которого раньше не было, открывается на запись или добавление, означает, что он создается (если такая процедура физически возможна). Открытие уже существующего файла на запись приводит к выбрасыванию его старого содержимого, в то время как при открытии файла на добавление его старое содержимое сохраняется. Попытка читать несуществующий файл является ошибкой. Могут иметь место и другие ошибки; например, ошибкой считается попытка чтения файла, который по статусу запрещено читать. При наличии любой ошибки fopen возвращает NULL. (Возможна более точная идентификация ошибки; детальная информация по этому поводу приводится в конце параграфа 1 приложения В.)
Следующее, что нам необходимо знать, — это как читать из файла или писать в файл, коль скоро он открыт. Существует несколько способов сделать это, из которых самый простой состоит в том, чтобы воспользоваться функциями getc и putc. Функция getc возвращает следующий символ из файла; ей необходимо сообщить указатель файла, чтобы она знала, откуда брать символ.
int getc(FILE *fp)
Функция getc возвращает следующий символ из потока, на который указывает *fp; в случае исчерпания файла или ошибки она возвращает EOF.
Функция putc пишет символ c в файл fp
int putc(int с, FILE *fp)
и возвращает записанный символ или EOF в случае ошибки. Аналогично getchar и putchar, реализация getc и putc может быть выполнена в виде макросов, а не функций.
При запуске Си-программы операционная система всегда открывает три файла и обеспечивает три файловые ссылки на них. Этими файлами являются: стандартный ввод, стандартный вывод и стандартный файл ошибок; соответствующие им указатели называются stdin, stdout и stderr; они описаны в <stdio.h>. Обычно stdin соотнесен с клавиатурой, a stdout и stderr — с экраном. Однако stdin и stdout можно связать с файлами или, используя конвейерный механизм, соединить напрямую с другими программами, как это описывалось в параграфе 7.1.
С помощью getc, putc, stdin и stdout функции getchar и putchar теперь можно определить следующим образом:
#define getchar() getc(stdin) #define putchar(c) putc((c), stdout)
Форматный ввод-вывод файлов можно построить на функциях fscanf и fprintf. Они идентичны scanf и printf с той лишь разницей, что первым их аргументом является указатель на файл, для которого осуществляется ввод-вывод, формат же указывается вторым аргументом.
int fscanf(FILE *fp, char *format, ...) int fprintf(FILE *fp, char *format, ...)
Вот теперь мы располагаем теми сведениями, которые достаточны для написания программы cat, предназначенной для конкатенации (последовательного соединения) файлов. Предлагаемая версия функции cat, как оказалось, удобна для многих программ. Если в командной строке присутствуют аргументы, они
рассматриваются как имена последовательно обрабатываемых файлов. Если аргументов нет, то обработке подвергается стандартный ввод.
#include <stdio.h>
/* cat: конкатенация файлов, версия 1 */ main(int argc, char *argv[])
{
FILE *fp;
void filecopy(FILE *, FILE *);
if (argc == 1) /* нет аргументов; копируется стандартный ввод */ filecopy(stdin, stdout);
else
while (--argc > 0)
if ((fp = fopen(*++argv, "r")) == NULL) { printf("cat: не могу открыть файл %s\n", *argv); return 1;
} else {
filecopy(fp, stdout); fclose(fp);
}
return 0;
}
/* filecopy: копирует файл ifp в файл ofp */ void filecopy(FILE *ifp, FILE *ofp)
{
int c;
while ((c = getc(ifp)) != EOF) putc(c, ofp);
}
Файловые указатели stdin и stdout представляют собой объекты типа FILE*. Это константы, а не переменные, следовательно, им нельзя ничего присваивать.
Функция
int fclose(FILE *fp)
— обратная по отношению к fopen; она разрывает связь между файловым указателем и внешним именем (которая раньше была установлена с помощью fopen), освобождая тем самым этот указатель для других файлов. Так как в большинстве операционных систем количество одновременно открытых одной программой файлов ограничено, то файловые указатели, если они больше не нужны, лучше освобождать, как это и делается в программе cat. Есть еще одна причина применить fclose к файлу вывода, это необходимость "опорожнить" буфер, в котором putc накопила предназначенные для вывода данные. При нормальном завершении работы программы для каждого открытого файла fclose вызывается автоматически. (Вы можете закрыть stdin и stdout, если они вам не нужны. Воспользовавшись библиотечной функцией freopen, их можно восстановить.)
7.6. Управление ошибками (stderr и exit)
Обработку ошибок в cat нельзя признать идеальной. Беда в том, что если файл по какой-либо причине недоступен, сообщение об этом мы получим по окончании конкатенируемого вывода. Это нас устроило бы, если бы вывод отправлялся только на экран, а не в файл или по конвейеру другой программе.
Чтобы лучше справиться с этой проблемой, программе помимо стандартного вывода 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(t); } 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.