Страница:
+ - * / . % & | ^ << >> == 1= <> <= >=
либо унарными операциями
- ~
либо тернарной операцией
?:
Круглые скобки могут использоваться для группировки, но не
для обращения к функциям.
В случае инициализаторов допускается большая (ударение
на букву о) свобода; кроме перечисленных выше константных
выражений можно также применять унарную операцию & к внешним
или статическим объектам и к внешним или статическим масси-
вам, имеющим в качестве индексов константное выражение.
Унарная операция & может быть также применена неявно, в
результате появления неиндексированных массивов и функций.
Основное правило заключается в том, что после вычисления
инициализатор должен становится либо константой, либо адре-
сом ранее описанного внешнего или статического объекта плюс
или минус константа.
Одним из достоинств языка Си считается переносимость
программ на Си, которая связана как с относительной машинной
-49-
независимостью самого языка, так и с совместимостью среды,
обеспечиваемой совместимыми с ОС UNIX операционными систе-
мами. Вместе с тем, при написании на языке Си таких прог-
рамм, которые не должны зависеть от конкретной ЭВМ, необхо-
димо учитывать то, что некоторые части языка Си по своей
сути машинно-зависимы. Следующее ниже перечисление потенци-
альных трудностей хотя и не являются всеобъемлющими, но
выделяет основные из них.
Вопросы, целиком связанные с аппаратным оборудованием,
такие как размер слова, свойства вещественной арифметики и
целого деления, не представляют особенных затруднений. Дру-
гие аспекты аппаратных средств находят свое отражение в раз-
личных реализациях. Некоторые из них, в частности, знаковое
расширение (преобразующее отрицательный символ в отрицатель-
ное целое) и порядок, в котором помещаются байты в слове,
представляют собой неприятность, которая должна тщательно
отслеживаться. Большинство остальных проблем этого типа не
вызывает сколько_нибудь значительных затруднений.
Число переменных типа register, которое фактически
может быть помещено в регистры, меняется от машины к машине,
также как и набор допустимых для них типов. Тем не менее все
компиляторы на своих машинах работают надлежащим образом;
лишние или недопустимые регистровые описания игнорируются.
Некоторые трудности возникают только при использовании
сомнительной практики программирования, или при использова-
нии особенностей конкретной реализации. Писать программы,
которые зависят от таких особенностей, чрезвычайно нера-
зумно.
Языком не указывается порядок вычисления аргументов
функций; они вычисляются справа налево на CM-ЭВМ и ЭВМ PDP-
11 и VAXR-11 фирмы DEC и слева направо на большинстве
остальных машин. Порядок, в котором происходят побочные
эффекты, также не специфицируется.
Так как символьные константы в действительности явля-
ются объектами типа int, допускается использование символь-
ных констант, состоящих из нескольких символов. Однако, пос-
кольку порядок, в котором символы приписываются к слову,
меняется от машины к машине, конкретная реализация оказыва-
ется весьма машинно_зависимой.
Порядок присваивания полей к словам и символов к целым
также зависит от ЭВМ. Такие различия незаметны для изолиро-
ванных программ, в которых не разрешено смешивать типы (пре-
образуя, например, указатель на int в указатель на char и
затем проверяя указываемую память), но должны учитываться
при согласовании с накладываемыми извне схемами памяти.
-50-
Язык, принятый на различных компиляторах, отличается
только незначительными деталями. Самое заметное отличие сос-
тоит в том, что используемый в настоящее время компилятор на
CM-ЭВМ не инициализирует структуры, которые содержат поля
битов, не имеет типа "unsigned char" и имеет некоторые огра-
ничения на операции присваивания в определенных контекстах,
связанных с использованием значения присваивания структур.
В старых программах можно встретить некоторые устарев-
шие конструкции. Хотя большинство версий компилятора поддер-
живает такие анахронизмы, они в конце концов исчезнут, оста-
вив за собой только проблемы переносимости.
В ранних версиях Си для проблем присваивания использо-
валась форма =оп, а не оп=, приводя к двусмысленностям,
типичным примером которых является
х =-1
где х фактически уменьшается, поскольку операции = и - при-
мыкают друг к другу, но что вполне могло рассматриваться и
как присваивание -1 к х.
Синтаксис инициализаторов изменился: раньше знак
равенства, с которого начинается инициализатор, отсутство-
вал, так что вместо
int х = 1;
использовалось
int х 1;
изменение было внесено из_за инициализации
int f (1+2)
которая достаточно сильно напоминает определение функции,
чтобы смутить компиляторы.
Средства ввода/вывода не являются составной частью
языка Си. В этой главе будет описана "стандартная библио-
тека ввода/вывода", то есть набор функций, разработанных для
обеспечения стандартной системы ввода/вывода для Си- прог-
рамм. Эти функции отражают только те операции, которые могут
быть обеспечены на большинстве современных операционных сис-
тем. Процедуры достаточно эффективны для того, чтобы пользо-
ватели редко чувствовали необходимость обойти их "ради
-51-
эффективности", как бы ни была важна конкретная задача. И
наконец, эти процедуры были задуманы авторами языка "перено-
симыми" в том смысле, что они должны существовать в совмес-
тимом виде на любой системе, где имеется язык Си, и что
программы, которые ограничивают свои взаимодействия с сис-
темными возможностями, предоставляемыми стандартной библио-
текой, можно будет переносить с одной системы на другую по
существу без изменений.
Далее описываются основные принципы организации
ввода/вывода в программах на языке Си, использующих библио-
теку ввода/вывода. Полное описание этой библиотеки имеется в
руководстве программиста (часть 4) или в оперативной доку-
ментации ("man(3)"). Программы, работающие в ОС ДЕМОС,
могут также обращаться к функциям ввода/вывода низкого
уровня, которые реализованы непосредственно в ядре ОС ДЕМОС,
но такая необходимость возникает достаточно редко.
Каждый исходный файл, который обращается к функции из
стандартной библиотеки, должен где то в начале содержать
строку
#include <stdio.h>
В файле stdio.h определяются некоторые макросы и переменные,
используемые библиотекой ввода/вывода.
Самый простой механизм ввода заключается в чтении по
одному символу за раз из "стандартного ввода" (обычно с тер-
минала пользователя) с помощью функции getchar. Функция
getchar() целого типа при каждом к ней обращении возвращает
следующий вводимый символ. В большинстве систем, которые
поддерживают язык Си, терминал может быть заменен некоторым
файлом с помощью обозначения "<". Если некоторая программа
prog использует функцию getchar, то командная строка
prog <infile
приведет к тому, что prog будет читать из файла infile, а не
с терминала. Переключение ввода делается таким образом, что
сама программа prog не замечает изменения; в частности
строка "<infile" не включается в командную строку аргументов
(см. следующую главу). Переключение ввода оказывается неза-
метным и в том случае, когда вывод поступает из другой прог-
раммы через межпроцессный канал. Например, командная строка
otherprog | prog
прогоняет две программы, otherprog и prog, так, что
-52-
стандартным вводом для prog служит стандартный вывод other-
prog.
Функция getchar возвращает значение EOF, когда достига-
ется конец файла, какой бы ввод она при этом не считывала.
Стандартная библиотека полагает символическую константу EOF
равной -1 (посредством #define в файле stdio.h), но проверки
следует писать в терминах EOF, а не -1, чтобы избежать зави-
симости от конкретного значения.
Вывод можно осуществлять с помощью функции putchar(c),
помещающей символ 'c' в "стандартный вывод", который по
умолчанию является терминалом. Вывод можно при вызове прог-
раммы направить в некоторый файл с помощью обозначения ">".
Если prog использует putchar, то командная строка
prog > outfile
приведет к записи стандартного вывода в файл outfile, а не
на терминал. В системе ДЕМОС можно также использовать межп-
роцессный канал.
В стандартной библиотеке ввода/вывода "функции" getchar
и putchar на самом деле могут быть макросами. Это позволяет
избежать накладных расходов на обращение к функции для обра-
ботки каждого символа.
Две функции: printf для вывода и scanf для ввода (сле-
дующий раздел) позволяют преобразовывать численные величины
в символьное представление и обратно. Они также позволяют
генерировать и интерпретировать форматные строки. Функция
printf(control, arg1, arg2, ...)
преобразует аргументы в текстовую форму в соответствии с
форматами, заданными в управляющей строке control, и выдает
результат в стандартный вывод. Управляющая строка содержит
два типа объектов: обычные символы, которые просто копиру-
ются в выходной поток, и спецификации преобразований, каждая
из которых вызывает преобразование и печать очередного аргу-
мента printf.
Каждая спецификация преобразования начинается с символа
"%" и заканчивается символом преобразования (буквой, опреде-
ляющей тип преобразования). Между "%" и символом преобразо-
вания могут находиться:
- Знак минус, который вызывает выравнивание преобразо-
ванного аргумента по левому краю поля.
-53-
- Строка цифр, задающая минимальную ширину поля. Преоб-
разованное число будет напечатано в поле по крайней
мере этой ширины, а если необходимо, то и в более
широком. Если преобразованный аргумент имеет меньше
символов, чем указанная ширина поля, то он будет
дополнен слева (или справа, если было указано вырав-
нивание по левому краю) заполняющими символами до
этой ширины. Заполняющим символом обычно является
пробел, а если ширина поля указывается с лидирующим
нулем, то этим символом будет нуль (лидирующий нуль в
данном случае не означает восьмеричной ширины поля).
- Точка, которая отделяет ширину поля от следующей
строки цифр.
- Строка цифр (точность); указывает максимальное число
символов строки, которые должны быть напечатаны, или
число печатаемых справа от десятичной точки цифр для
переменных типа float или double.
- Модификатор длины l, который указывает, что соот-
ветствующий элемент данных имеет тип long, а не int.
Ниже приводятся символы преобразования и их смысл:
d - аргумент преобразуется к десятичному виду;
o - аргумент преобразуется в беззнаковую восьмеричную
форму (без лидирующего нуля);
x - аргумент преобразуется в беззнаковую шестнадцатерич-
ную форму (без лидирующих 0х);
u - аргумент преобразуется в беззнаковую десятичную
форму;
c - аргумент рассматривается как отдельный символ;
s - аргумент является строкой: символы строки печатаются
до тех пор, пока не будет достигнут нулевой символ
или не будет напечатано количество символов, указан-
ное в спецификации точности;
e - аргумент, рассматриваемый как переменная типа float
или double, преобразуется в десятичную форму в виде
[-]m.nnnnnne[+-]хх, где длина строки из n определя-
ется указанной точностью. Точность по умолчанию равна
6;
f - аргумент, рассматриваемый как переменная типа float
или double, преобразуется в десятичную форму в виде
[-]mmm.nnnnn, где длина строки из n определяется ука-
занной точностью. Точность по умолчанию равна 6.
-54-
Отметим, что эта точность не определяет количество
печатаемых в формате f значащих цифр;
g - используется или формат %e или %f, какой короче; нез-
начащие нули не печатаются.
Вместо "ld"можно использовать "D", вместо "lo" - "O", вместо
"lx" - "X".
Если идущий за % символ не является символом преобразо-
вания, то печатается сам этот символ; следовательно,символ %
можно напечатать, указав %%.
Большинство из форматных преобразований очевидно.
Единственным исключением является то, как точность взаимо-
действует со строками. Следующая таблица демонстрирует влия-
ние различных спецификаций на печать "hello, world" (12 сим-
волов). Вокруг каждого поля помещены двоеточия для того,
чтобы можно было определить его протяженность.
:%10s: :hello, world:
:%10-s: :hello, world:
:%20s: : hello, world:
:%-20s: :hello, world :
:%20.10s: : hello, wor:
:%-20.10s: :hello, wor :
:%.10s: :hello, wor:
ПРЕДОСТЕРЕЖЕНИЕ: printf использует свой первый аргумент для
определения числа последующих аргументов и их типов. Если
количество аргументов окажется недостаточным или они будут
иметь несоответствующие типы, то возникнет путаница и
результаты будут неверными.
Осуществляющая ввод функция scanf является аналогом
printf и позволяет проводить в обратном направлении многие
из тех же самых преобразований. Функция
scanf(control, arg1, arg2, ...)
читает символы из стандартного ввода, интерпретирует их в
соответствии с форматом, указанном в аргументе control, и
помещает результаты в остальные аргументы. Управляющая
строка описывается ниже; другие аргументы, каждый из которых
должен быть указателем, определяют, куда следует поместить
соответствующим образом преобразованный ввод.
Управляющая строка обычно содержит спецификации преоб-
разования, которые используются для непосредственной интерп-
ретации входных последовательностей. Управляющая строка
-55-
может содержать:
- пробелы, табуляции или символы новой строки ("символы
пустых промежутков"), которые игнорируются;
- обычные символы (не %), которые предполагаются совпа-
дающими со следующими отличными от "символов пустых
промежутков" символами входного потока;
- спецификации преобразования, состоящие из символа %,
необязательного символа подавления присваивания *,
необязательного числа, задающего максимальную ширину
поля и символа преобразования.
Спецификация преобразования управляет преобразованием
следующего поля ввода. Обычно результат помещается в пере-
менную, которая указывается соответствующим аргументом.
Если, однако , с помощью символа * указано подавление прис-
ваивания, то это поле ввода просто пропускается и никакого
присваивания не производится. Поле ввода определяется как
строка символов, которые отличны от "символов простых проме-
жутков"; оно продолжается либо до следующего символа пустого
промежутка, либо пока не будет исчерпана ширина поля, если
она указана. Отсюда следует, что при поиске нужного ей
ввода, функция scanf будет пересекать границы строк, пос-
кольку символ новой строки является одним из символов пустых
промежутков.
Имеется возможность задания более сложного алгоритма
выделения полей ввода, которая описана в руководстве прог-
раммиста ("scanf(3)").
Символ преобразования определяет интерпретацию поля
ввода; поскольку в Си аргументы передаются по значению,
аргументы scanf должны быть указателями. Допускаются следую-
щие символы преобразования:
d - На вводе ожидается десятичное целое; соответствующий
аргумент должен быть указателем на целое.
o - На вводе ожидается восьмеричное целое (с лидирующим
нулем или без него); соответствующий аргумент должен
быть указателем на целое.
x - На вводе ожидается шестнадцатиричное целое (с лидиру-
ющими 0х или без них); соответствующий аргумент дол-
жен быть указателем на целое.
h - На вводе ожидается целое типа short; соответствующий
аргумент должен быть указателем на целое типа short.
c - Ожидается отдельный символ; соответствующий аргумент
должен быть указателем на символы; следующий вводимый
-56-
символ помещается в указанное место. Обычный пропуск
символов пустых промежутков в этом случае подавля-
ется; для чтения следующего символа, который не явля-
ется символом пустого промежутка, пользуйтесь специ-
фикацией преобразования %1s.
s - Ожидается символьная строка; соответствующий аргумент
должен быть указателем символов, который указывает на
массив символов, достаточно большой для принятия
строки и добавляемого в конце символа \0.
f - Ожидается число с вещественной точкой; соответствую-
щий аргумент должен быть указателем на переменную
типа float.
e - Символ преобразования e является синонимом для f.
Формат ввода переменной типа float включает необяза-
тельный знак, строку цифр, возможно содержащую деся-
тичную точку и необязательное поле экспоненты, состо-
ящее из буквы e, за которой следует целое, возможно
имеющее знак.
Перед символами преобразования d, o и x может стоять
буква l, которая означает, что в списке аргументов должен
находиться указатель на переменную типа long, а не типа int.
Аналогично, буква l может стоять перед символами преобразо-
вания e или f, говоря о том, что в списке аргументов должен
находиться указатель на переменную типа double, а не типа
float.
Например, обращение
int 1;
float х;
char name[50];
scanf("%d %f %s", &i, &х, name);
со строкой на вводе
25 54.32e-1 thompson
приводит к присваиванию i значения 25, х - значения 5.432 и
name - строки "thompson", надлежащим образом законченной
символом \0. Эти три поля ввода можно разделить произволь-
ным числом пробелов, табуляций и символов новой строки,
сколько вы пожелаете. Обращение
int i;
float х;
char name[50];
scanf("%2d %f %*d %2s", &i, &х, name);
с вводом
-57-
56789 0123 45а72
присвоит i значение 56, х - 789.0, пропустит 0123 и поместит
в name строку "45". При следующем обращении к любой проце-
дуре ввода рассмотрение начнется с буквы a. В этих двух при-
мерах name является указателем и, следовательно, перед ним
не нужно помещать знак &.
В качестве другого примера приведем программу для сум-
мирования чисел, вводимых с терминала:
#include <stdio.h>
main() /* Примитивный калькулятор */
{
double sum, v;
sum =0;
while (scanf("%lf", &v) !=EOF)
printf("\t%.2fFI\n", sum += v);
}
Выполнение функции scanf заканчивается либо тогда, когда она
исчерпывает свою управляющую строку, либо когда некоторый
элемент ввода не соответствует очередной спецификации преоб-
разования. В качестве своего значения она возвращает число
правильно распознанных элементов ввода. Это число может быть
использовано для определения количества найденных элементов
ввода. При выходе на конец файла возвращается EOF; подчерк-
нем, что это значение отлично от 0, означающего, что следую-
щий вводимый символ не удовлетворяет первой спецификации в
управляющей строке. При следующем обращении к scanf поиск
возобновляется непосредственно за последним введенным симво-
лом.
ПРЕДОСТЕРЕЖЕНИЕ: аргументы функции scanf должны быть
указателями. Несомненно, наиболее распространенная ошибка
состоит в написании
scanf("%d", n);
вместо
scanf("%d", &n);
От функции scanf и printf происходят функции sscanf и
sprintf, которые осуществляют аналогичные преобразования, но
оперируют со строкой, а не с файлом. Обращения к этим функ-
циям имеют вид:
-58-
sprintf(string, control, arg1, arg2, ...)
sscanf(string, control, arg1, arg2, ...)
Как и раньше , функция sprintf преобразует свои аргументы
arg1, arg2 и т.д. в соответствии с форматом, указанным в
control, но помещает результаты в string, а не в стандартный
вывод. Конечно, строка string должна быть достаточно велика,
чтобы принять результат. Например, если name - это символь-
ный массив, а n - целое, то
sprintf(name, "temp%d", n);
создает в name строку вида "tempnnn", где nnn - значение n.
Функция sscanf выполняет обратные преобразования - она
просматривает строку string в соответствии с форматом в
аргументе control и помещает результирующие значения в аргу-
менты arg1, arg2 и т.д. Эти аргументы должны быть указате-
лями. В результате обращения
sscanf(name, "temp%d", &n);
переменная n получает значение строки цифр, следующих за
temp в name.
Описанные в начале данного раздела программы читают из
стандартного ввода и пишут в стандартный вывод, которые пре-
доставляются программе операционной системой.
Для программ, которые сами должны организовывать связь
с файлами, в библиотеке ввода/вывода действуют следующие
правила.
Прежде чем считывать из некоторого файла или записы-
вать в него, этот файл должен быть открыт с помощью функции
open из стандартной библиотеки. Функция fopen берет внешнее
имя (подобное х.c или "temp002") и возвращает внутреннее
имя, которое должно использоваться при последующих чтениях
из файла или записях в него.
Это внутреннее имя, называемое "указателем файла", фак-
тически является указателем структуры, которая содержит
информацию о файле, такую как место размещения буфера, теку-
щая позиция символа в буфере, происходит ли чтение из файла
или запись в него и тому подобное. Пользователи не обязаны
знать эти детали, потому что среди определений, получаемых
из файла stdio.h, содержится определение этой структуры.
Единственное необходимое для указателя файла описание
демонстрируется примером: FILE *fp;
-59-
Здесь говорится, что fp является указателем на FILE.
Обратите внимание, что file является именем типа, подобным
int, а не ярлыком структуры; это реализовано через
"#define".
Обращение к функции fopen в программе имеет вид:
fp=fopen(name,mode);
Первым аргументом функции fopen является имя файла, которое
задается в виде символьной сроки "name". Второй аргумент
mode (режим) также является символьной строкой, которая ука-
зывает, как этот файл будет использоваться. Допустимыми
режимами являются: чтение (r), запись (w) и добавление (a),
возможен еще символ + справа (например, r+), который озна-
чает, что возможно и чтение, и запись в файл.
Если вы откроете файл, который еще не существует, для
записи или добавления, то такой файл будет создан (если это
возможно). Открытие существующего файла на запись приводит
к отбрасыванию его старого содержимого. Попытка чтения несу-
ществующего файла является ощибкой. Ошибки могут быть обус-
ловлены и другими причинами (например, попытка чтения из
файла, не имея на то разрешения). При наличии какой-либо
ошибки функция возвращает нулевое значение указателя NULL
(которое для удобства также определяется в файле stdio.h).
Другой необходимой вещью является способ чтения или
записи, если файл уже открыт. Здесь имеется несколько воз-
можностей, из которых getc и putc являются простейшими.
Функция getc считывает из файла следующий символ; ей необхо-
дим указатель файла, чтобы знать, из какого файла читать.
Обращение: c=getc(fp)
помещает в c следующий символ из файла, указанного посредст-
вом fp, и EOF, если достигнут конец файла.
Функция putc: putc(c,fp)
помещает символ c в файл fp и возвращает c. Подобно функциям
getchar и putchar, getc и putc могут быть макросами, а не
функциями.
При запуске программы автоматически открываются три
файла, которые снабжены определенными указателями файлов.
Этими файлами являются стандартный ввод, стандартный вывод и
стандартный вывод ошибок; соответствующие указатели файлов
называются stdin, stdout и stderr. Обычно все эти указатели
связаны с терминалом, но stdin и stdout могут быть перенап-
равлены на файлы или в межпроцессный канал.
Функции getchar и putchar могут быть определены в тер-
минах getc, putc, stdin и stdout следующим образом:
#define getchar() getc(stdin)
#define putchar(c) putc(c, stdout)
-60-
При работе с файлами для форматного ввода и вывода можно
использовать функции fscanf и fprintf. Они идентичны функ-
циям scanf и printf, за исключением того, что первым аргу-
ментом является указатель файла, определяющий тот файл,
который будет читаться или куда будет вестись запись; управ-
либо унарными операциями
- ~
либо тернарной операцией
?:
Круглые скобки могут использоваться для группировки, но не
для обращения к функциям.
В случае инициализаторов допускается большая (ударение
на букву о) свобода; кроме перечисленных выше константных
выражений можно также применять унарную операцию & к внешним
или статическим объектам и к внешним или статическим масси-
вам, имеющим в качестве индексов константное выражение.
Унарная операция & может быть также применена неявно, в
результате появления неиндексированных массивов и функций.
Основное правило заключается в том, что после вычисления
инициализатор должен становится либо константой, либо адре-
сом ранее описанного внешнего или статического объекта плюс
или минус константа.
Одним из достоинств языка Си считается переносимость
программ на Си, которая связана как с относительной машинной
-49-
независимостью самого языка, так и с совместимостью среды,
обеспечиваемой совместимыми с ОС UNIX операционными систе-
мами. Вместе с тем, при написании на языке Си таких прог-
рамм, которые не должны зависеть от конкретной ЭВМ, необхо-
димо учитывать то, что некоторые части языка Си по своей
сути машинно-зависимы. Следующее ниже перечисление потенци-
альных трудностей хотя и не являются всеобъемлющими, но
выделяет основные из них.
Вопросы, целиком связанные с аппаратным оборудованием,
такие как размер слова, свойства вещественной арифметики и
целого деления, не представляют особенных затруднений. Дру-
гие аспекты аппаратных средств находят свое отражение в раз-
личных реализациях. Некоторые из них, в частности, знаковое
расширение (преобразующее отрицательный символ в отрицатель-
ное целое) и порядок, в котором помещаются байты в слове,
представляют собой неприятность, которая должна тщательно
отслеживаться. Большинство остальных проблем этого типа не
вызывает сколько_нибудь значительных затруднений.
Число переменных типа register, которое фактически
может быть помещено в регистры, меняется от машины к машине,
также как и набор допустимых для них типов. Тем не менее все
компиляторы на своих машинах работают надлежащим образом;
лишние или недопустимые регистровые описания игнорируются.
Некоторые трудности возникают только при использовании
сомнительной практики программирования, или при использова-
нии особенностей конкретной реализации. Писать программы,
которые зависят от таких особенностей, чрезвычайно нера-
зумно.
Языком не указывается порядок вычисления аргументов
функций; они вычисляются справа налево на CM-ЭВМ и ЭВМ PDP-
11 и VAXR-11 фирмы DEC и слева направо на большинстве
остальных машин. Порядок, в котором происходят побочные
эффекты, также не специфицируется.
Так как символьные константы в действительности явля-
ются объектами типа int, допускается использование символь-
ных констант, состоящих из нескольких символов. Однако, пос-
кольку порядок, в котором символы приписываются к слову,
меняется от машины к машине, конкретная реализация оказыва-
ется весьма машинно_зависимой.
Порядок присваивания полей к словам и символов к целым
также зависит от ЭВМ. Такие различия незаметны для изолиро-
ванных программ, в которых не разрешено смешивать типы (пре-
образуя, например, указатель на int в указатель на char и
затем проверяя указываемую память), но должны учитываться
при согласовании с накладываемыми извне схемами памяти.
-50-
Язык, принятый на различных компиляторах, отличается
только незначительными деталями. Самое заметное отличие сос-
тоит в том, что используемый в настоящее время компилятор на
CM-ЭВМ не инициализирует структуры, которые содержат поля
битов, не имеет типа "unsigned char" и имеет некоторые огра-
ничения на операции присваивания в определенных контекстах,
связанных с использованием значения присваивания структур.
В старых программах можно встретить некоторые устарев-
шие конструкции. Хотя большинство версий компилятора поддер-
живает такие анахронизмы, они в конце концов исчезнут, оста-
вив за собой только проблемы переносимости.
В ранних версиях Си для проблем присваивания использо-
валась форма =оп, а не оп=, приводя к двусмысленностям,
типичным примером которых является
х =-1
где х фактически уменьшается, поскольку операции = и - при-
мыкают друг к другу, но что вполне могло рассматриваться и
как присваивание -1 к х.
Синтаксис инициализаторов изменился: раньше знак
равенства, с которого начинается инициализатор, отсутство-
вал, так что вместо
int х = 1;
использовалось
int х 1;
изменение было внесено из_за инициализации
int f (1+2)
которая достаточно сильно напоминает определение функции,
чтобы смутить компиляторы.
Средства ввода/вывода не являются составной частью
языка Си. В этой главе будет описана "стандартная библио-
тека ввода/вывода", то есть набор функций, разработанных для
обеспечения стандартной системы ввода/вывода для Си- прог-
рамм. Эти функции отражают только те операции, которые могут
быть обеспечены на большинстве современных операционных сис-
тем. Процедуры достаточно эффективны для того, чтобы пользо-
ватели редко чувствовали необходимость обойти их "ради
-51-
эффективности", как бы ни была важна конкретная задача. И
наконец, эти процедуры были задуманы авторами языка "перено-
симыми" в том смысле, что они должны существовать в совмес-
тимом виде на любой системе, где имеется язык Си, и что
программы, которые ограничивают свои взаимодействия с сис-
темными возможностями, предоставляемыми стандартной библио-
текой, можно будет переносить с одной системы на другую по
существу без изменений.
Далее описываются основные принципы организации
ввода/вывода в программах на языке Си, использующих библио-
теку ввода/вывода. Полное описание этой библиотеки имеется в
руководстве программиста (часть 4) или в оперативной доку-
ментации ("man(3)"). Программы, работающие в ОС ДЕМОС,
могут также обращаться к функциям ввода/вывода низкого
уровня, которые реализованы непосредственно в ядре ОС ДЕМОС,
но такая необходимость возникает достаточно редко.
Каждый исходный файл, который обращается к функции из
стандартной библиотеки, должен где то в начале содержать
строку
#include <stdio.h>
В файле stdio.h определяются некоторые макросы и переменные,
используемые библиотекой ввода/вывода.
Самый простой механизм ввода заключается в чтении по
одному символу за раз из "стандартного ввода" (обычно с тер-
минала пользователя) с помощью функции getchar. Функция
getchar() целого типа при каждом к ней обращении возвращает
следующий вводимый символ. В большинстве систем, которые
поддерживают язык Си, терминал может быть заменен некоторым
файлом с помощью обозначения "<". Если некоторая программа
prog использует функцию getchar, то командная строка
prog <infile
приведет к тому, что prog будет читать из файла infile, а не
с терминала. Переключение ввода делается таким образом, что
сама программа prog не замечает изменения; в частности
строка "<infile" не включается в командную строку аргументов
(см. следующую главу). Переключение ввода оказывается неза-
метным и в том случае, когда вывод поступает из другой прог-
раммы через межпроцессный канал. Например, командная строка
otherprog | prog
прогоняет две программы, otherprog и prog, так, что
-52-
стандартным вводом для prog служит стандартный вывод other-
prog.
Функция getchar возвращает значение EOF, когда достига-
ется конец файла, какой бы ввод она при этом не считывала.
Стандартная библиотека полагает символическую константу EOF
равной -1 (посредством #define в файле stdio.h), но проверки
следует писать в терминах EOF, а не -1, чтобы избежать зави-
симости от конкретного значения.
Вывод можно осуществлять с помощью функции putchar(c),
помещающей символ 'c' в "стандартный вывод", который по
умолчанию является терминалом. Вывод можно при вызове прог-
раммы направить в некоторый файл с помощью обозначения ">".
Если prog использует putchar, то командная строка
prog > outfile
приведет к записи стандартного вывода в файл outfile, а не
на терминал. В системе ДЕМОС можно также использовать межп-
роцессный канал.
В стандартной библиотеке ввода/вывода "функции" getchar
и putchar на самом деле могут быть макросами. Это позволяет
избежать накладных расходов на обращение к функции для обра-
ботки каждого символа.
Две функции: printf для вывода и scanf для ввода (сле-
дующий раздел) позволяют преобразовывать численные величины
в символьное представление и обратно. Они также позволяют
генерировать и интерпретировать форматные строки. Функция
printf(control, arg1, arg2, ...)
преобразует аргументы в текстовую форму в соответствии с
форматами, заданными в управляющей строке control, и выдает
результат в стандартный вывод. Управляющая строка содержит
два типа объектов: обычные символы, которые просто копиру-
ются в выходной поток, и спецификации преобразований, каждая
из которых вызывает преобразование и печать очередного аргу-
мента printf.
Каждая спецификация преобразования начинается с символа
"%" и заканчивается символом преобразования (буквой, опреде-
ляющей тип преобразования). Между "%" и символом преобразо-
вания могут находиться:
- Знак минус, который вызывает выравнивание преобразо-
ванного аргумента по левому краю поля.
-53-
- Строка цифр, задающая минимальную ширину поля. Преоб-
разованное число будет напечатано в поле по крайней
мере этой ширины, а если необходимо, то и в более
широком. Если преобразованный аргумент имеет меньше
символов, чем указанная ширина поля, то он будет
дополнен слева (или справа, если было указано вырав-
нивание по левому краю) заполняющими символами до
этой ширины. Заполняющим символом обычно является
пробел, а если ширина поля указывается с лидирующим
нулем, то этим символом будет нуль (лидирующий нуль в
данном случае не означает восьмеричной ширины поля).
- Точка, которая отделяет ширину поля от следующей
строки цифр.
- Строка цифр (точность); указывает максимальное число
символов строки, которые должны быть напечатаны, или
число печатаемых справа от десятичной точки цифр для
переменных типа float или double.
- Модификатор длины l, который указывает, что соот-
ветствующий элемент данных имеет тип long, а не int.
Ниже приводятся символы преобразования и их смысл:
d - аргумент преобразуется к десятичному виду;
o - аргумент преобразуется в беззнаковую восьмеричную
форму (без лидирующего нуля);
x - аргумент преобразуется в беззнаковую шестнадцатерич-
ную форму (без лидирующих 0х);
u - аргумент преобразуется в беззнаковую десятичную
форму;
c - аргумент рассматривается как отдельный символ;
s - аргумент является строкой: символы строки печатаются
до тех пор, пока не будет достигнут нулевой символ
или не будет напечатано количество символов, указан-
ное в спецификации точности;
e - аргумент, рассматриваемый как переменная типа float
или double, преобразуется в десятичную форму в виде
[-]m.nnnnnne[+-]хх, где длина строки из n определя-
ется указанной точностью. Точность по умолчанию равна
6;
f - аргумент, рассматриваемый как переменная типа float
или double, преобразуется в десятичную форму в виде
[-]mmm.nnnnn, где длина строки из n определяется ука-
занной точностью. Точность по умолчанию равна 6.
-54-
Отметим, что эта точность не определяет количество
печатаемых в формате f значащих цифр;
g - используется или формат %e или %f, какой короче; нез-
начащие нули не печатаются.
Вместо "ld"можно использовать "D", вместо "lo" - "O", вместо
"lx" - "X".
Если идущий за % символ не является символом преобразо-
вания, то печатается сам этот символ; следовательно,символ %
можно напечатать, указав %%.
Большинство из форматных преобразований очевидно.
Единственным исключением является то, как точность взаимо-
действует со строками. Следующая таблица демонстрирует влия-
ние различных спецификаций на печать "hello, world" (12 сим-
волов). Вокруг каждого поля помещены двоеточия для того,
чтобы можно было определить его протяженность.
:%10s: :hello, world:
:%10-s: :hello, world:
:%20s: : hello, world:
:%-20s: :hello, world :
:%20.10s: : hello, wor:
:%-20.10s: :hello, wor :
:%.10s: :hello, wor:
ПРЕДОСТЕРЕЖЕНИЕ: printf использует свой первый аргумент для
определения числа последующих аргументов и их типов. Если
количество аргументов окажется недостаточным или они будут
иметь несоответствующие типы, то возникнет путаница и
результаты будут неверными.
Осуществляющая ввод функция scanf является аналогом
printf и позволяет проводить в обратном направлении многие
из тех же самых преобразований. Функция
scanf(control, arg1, arg2, ...)
читает символы из стандартного ввода, интерпретирует их в
соответствии с форматом, указанном в аргументе control, и
помещает результаты в остальные аргументы. Управляющая
строка описывается ниже; другие аргументы, каждый из которых
должен быть указателем, определяют, куда следует поместить
соответствующим образом преобразованный ввод.
Управляющая строка обычно содержит спецификации преоб-
разования, которые используются для непосредственной интерп-
ретации входных последовательностей. Управляющая строка
-55-
может содержать:
- пробелы, табуляции или символы новой строки ("символы
пустых промежутков"), которые игнорируются;
- обычные символы (не %), которые предполагаются совпа-
дающими со следующими отличными от "символов пустых
промежутков" символами входного потока;
- спецификации преобразования, состоящие из символа %,
необязательного символа подавления присваивания *,
необязательного числа, задающего максимальную ширину
поля и символа преобразования.
Спецификация преобразования управляет преобразованием
следующего поля ввода. Обычно результат помещается в пере-
менную, которая указывается соответствующим аргументом.
Если, однако , с помощью символа * указано подавление прис-
ваивания, то это поле ввода просто пропускается и никакого
присваивания не производится. Поле ввода определяется как
строка символов, которые отличны от "символов простых проме-
жутков"; оно продолжается либо до следующего символа пустого
промежутка, либо пока не будет исчерпана ширина поля, если
она указана. Отсюда следует, что при поиске нужного ей
ввода, функция scanf будет пересекать границы строк, пос-
кольку символ новой строки является одним из символов пустых
промежутков.
Имеется возможность задания более сложного алгоритма
выделения полей ввода, которая описана в руководстве прог-
раммиста ("scanf(3)").
Символ преобразования определяет интерпретацию поля
ввода; поскольку в Си аргументы передаются по значению,
аргументы scanf должны быть указателями. Допускаются следую-
щие символы преобразования:
d - На вводе ожидается десятичное целое; соответствующий
аргумент должен быть указателем на целое.
o - На вводе ожидается восьмеричное целое (с лидирующим
нулем или без него); соответствующий аргумент должен
быть указателем на целое.
x - На вводе ожидается шестнадцатиричное целое (с лидиру-
ющими 0х или без них); соответствующий аргумент дол-
жен быть указателем на целое.
h - На вводе ожидается целое типа short; соответствующий
аргумент должен быть указателем на целое типа short.
c - Ожидается отдельный символ; соответствующий аргумент
должен быть указателем на символы; следующий вводимый
-56-
символ помещается в указанное место. Обычный пропуск
символов пустых промежутков в этом случае подавля-
ется; для чтения следующего символа, который не явля-
ется символом пустого промежутка, пользуйтесь специ-
фикацией преобразования %1s.
s - Ожидается символьная строка; соответствующий аргумент
должен быть указателем символов, который указывает на
массив символов, достаточно большой для принятия
строки и добавляемого в конце символа \0.
f - Ожидается число с вещественной точкой; соответствую-
щий аргумент должен быть указателем на переменную
типа float.
e - Символ преобразования e является синонимом для f.
Формат ввода переменной типа float включает необяза-
тельный знак, строку цифр, возможно содержащую деся-
тичную точку и необязательное поле экспоненты, состо-
ящее из буквы e, за которой следует целое, возможно
имеющее знак.
Перед символами преобразования d, o и x может стоять
буква l, которая означает, что в списке аргументов должен
находиться указатель на переменную типа long, а не типа int.
Аналогично, буква l может стоять перед символами преобразо-
вания e или f, говоря о том, что в списке аргументов должен
находиться указатель на переменную типа double, а не типа
float.
Например, обращение
int 1;
float х;
char name[50];
scanf("%d %f %s", &i, &х, name);
со строкой на вводе
25 54.32e-1 thompson
приводит к присваиванию i значения 25, х - значения 5.432 и
name - строки "thompson", надлежащим образом законченной
символом \0. Эти три поля ввода можно разделить произволь-
ным числом пробелов, табуляций и символов новой строки,
сколько вы пожелаете. Обращение
int i;
float х;
char name[50];
scanf("%2d %f %*d %2s", &i, &х, name);
с вводом
-57-
56789 0123 45а72
присвоит i значение 56, х - 789.0, пропустит 0123 и поместит
в name строку "45". При следующем обращении к любой проце-
дуре ввода рассмотрение начнется с буквы a. В этих двух при-
мерах name является указателем и, следовательно, перед ним
не нужно помещать знак &.
В качестве другого примера приведем программу для сум-
мирования чисел, вводимых с терминала:
#include <stdio.h>
main() /* Примитивный калькулятор */
{
double sum, v;
sum =0;
while (scanf("%lf", &v) !=EOF)
printf("\t%.2fFI\n", sum += v);
}
Выполнение функции scanf заканчивается либо тогда, когда она
исчерпывает свою управляющую строку, либо когда некоторый
элемент ввода не соответствует очередной спецификации преоб-
разования. В качестве своего значения она возвращает число
правильно распознанных элементов ввода. Это число может быть
использовано для определения количества найденных элементов
ввода. При выходе на конец файла возвращается EOF; подчерк-
нем, что это значение отлично от 0, означающего, что следую-
щий вводимый символ не удовлетворяет первой спецификации в
управляющей строке. При следующем обращении к scanf поиск
возобновляется непосредственно за последним введенным симво-
лом.
ПРЕДОСТЕРЕЖЕНИЕ: аргументы функции scanf должны быть
указателями. Несомненно, наиболее распространенная ошибка
состоит в написании
scanf("%d", n);
вместо
scanf("%d", &n);
От функции scanf и printf происходят функции sscanf и
sprintf, которые осуществляют аналогичные преобразования, но
оперируют со строкой, а не с файлом. Обращения к этим функ-
циям имеют вид:
-58-
sprintf(string, control, arg1, arg2, ...)
sscanf(string, control, arg1, arg2, ...)
Как и раньше , функция sprintf преобразует свои аргументы
arg1, arg2 и т.д. в соответствии с форматом, указанным в
control, но помещает результаты в string, а не в стандартный
вывод. Конечно, строка string должна быть достаточно велика,
чтобы принять результат. Например, если name - это символь-
ный массив, а n - целое, то
sprintf(name, "temp%d", n);
создает в name строку вида "tempnnn", где nnn - значение n.
Функция sscanf выполняет обратные преобразования - она
просматривает строку string в соответствии с форматом в
аргументе control и помещает результирующие значения в аргу-
менты arg1, arg2 и т.д. Эти аргументы должны быть указате-
лями. В результате обращения
sscanf(name, "temp%d", &n);
переменная n получает значение строки цифр, следующих за
temp в name.
Описанные в начале данного раздела программы читают из
стандартного ввода и пишут в стандартный вывод, которые пре-
доставляются программе операционной системой.
Для программ, которые сами должны организовывать связь
с файлами, в библиотеке ввода/вывода действуют следующие
правила.
Прежде чем считывать из некоторого файла или записы-
вать в него, этот файл должен быть открыт с помощью функции
open из стандартной библиотеки. Функция fopen берет внешнее
имя (подобное х.c или "temp002") и возвращает внутреннее
имя, которое должно использоваться при последующих чтениях
из файла или записях в него.
Это внутреннее имя, называемое "указателем файла", фак-
тически является указателем структуры, которая содержит
информацию о файле, такую как место размещения буфера, теку-
щая позиция символа в буфере, происходит ли чтение из файла
или запись в него и тому подобное. Пользователи не обязаны
знать эти детали, потому что среди определений, получаемых
из файла stdio.h, содержится определение этой структуры.
Единственное необходимое для указателя файла описание
демонстрируется примером: FILE *fp;
-59-
Здесь говорится, что fp является указателем на FILE.
Обратите внимание, что file является именем типа, подобным
int, а не ярлыком структуры; это реализовано через
"#define".
Обращение к функции fopen в программе имеет вид:
fp=fopen(name,mode);
Первым аргументом функции fopen является имя файла, которое
задается в виде символьной сроки "name". Второй аргумент
mode (режим) также является символьной строкой, которая ука-
зывает, как этот файл будет использоваться. Допустимыми
режимами являются: чтение (r), запись (w) и добавление (a),
возможен еще символ + справа (например, r+), который озна-
чает, что возможно и чтение, и запись в файл.
Если вы откроете файл, который еще не существует, для
записи или добавления, то такой файл будет создан (если это
возможно). Открытие существующего файла на запись приводит
к отбрасыванию его старого содержимого. Попытка чтения несу-
ществующего файла является ощибкой. Ошибки могут быть обус-
ловлены и другими причинами (например, попытка чтения из
файла, не имея на то разрешения). При наличии какой-либо
ошибки функция возвращает нулевое значение указателя NULL
(которое для удобства также определяется в файле stdio.h).
Другой необходимой вещью является способ чтения или
записи, если файл уже открыт. Здесь имеется несколько воз-
можностей, из которых getc и putc являются простейшими.
Функция getc считывает из файла следующий символ; ей необхо-
дим указатель файла, чтобы знать, из какого файла читать.
Обращение: c=getc(fp)
помещает в c следующий символ из файла, указанного посредст-
вом fp, и EOF, если достигнут конец файла.
Функция putc: putc(c,fp)
помещает символ c в файл fp и возвращает c. Подобно функциям
getchar и putchar, getc и putc могут быть макросами, а не
функциями.
При запуске программы автоматически открываются три
файла, которые снабжены определенными указателями файлов.
Этими файлами являются стандартный ввод, стандартный вывод и
стандартный вывод ошибок; соответствующие указатели файлов
называются stdin, stdout и stderr. Обычно все эти указатели
связаны с терминалом, но stdin и stdout могут быть перенап-
равлены на файлы или в межпроцессный канал.
Функции getchar и putchar могут быть определены в тер-
минах getc, putc, stdin и stdout следующим образом:
#define getchar() getc(stdin)
#define putchar(c) putc(c, stdout)
-60-
При работе с файлами для форматного ввода и вывода можно
использовать функции fscanf и fprintf. Они идентичны функ-
циям scanf и printf, за исключением того, что первым аргу-
ментом является указатель файла, определяющий тот файл,
который будет читаться или куда будет вестись запись; управ-