Содержание

Глава 1. Обзор языка

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

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

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

1.1. Начнем, пожалуй

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

Напечатайте слова
    здравствуй, мир

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

Си-программа, печатающая «здравствуй, мир», выглядит так:

#include <stdio.h>
main()
{
    printf("здравствуй, мир\n");
}

Как запустить эту программу, зависит от системы, которую вы используете. Так, в операционной системе UNIX необходимо сформировать исходную программу в файле с именем, заканчивающимся символами «.c», например, в файле hello.c, который затем требуется скомпилировать с помощью команды

cc hello.c

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

a.out

программа напечатает

здравствуй, мир

В других системах правила запуска программы на выполнение могут быть иными; чтобы узнать о них, поговорите со специалистами.

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

Функции в Си похожи на подпрограммы и функции Фортрана и на процедуры и функции Паскаля. Приведенная программа — это функция с именем main. Обычно вы вольны придумывать любые имена для своих функций, но «main» — особое имя: любая программа начинает свои вычисления с первой инструкции функции main.

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

#include <stdio.h>

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

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

Первая программа на Си

#include <stdio.h>
Включение информации о стандартной библиотеке.
main()
Определение функции с именем main, не получающей никаких аргументов.
{
Инструкции main заключаются в фигурные скобки.
    printf("здравствуй, мир\n");
}
Функция main обращается к библиотечной функции printf для печати заданной последовательности литер, \n — литера новая_строка.

Инструкции функции заключаются в фигурные скобки { }. Функция main содержит только одну инструкцию

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

Функция вызывается по имени, после которого, в скобках, указывается список аргументов. Таким образом, приведенная строка — это вызов функции printf с аргументом "здравствуй, мир\n". printf — библиотечная функция, которая в данном случае напечатает последовательность литер, заключенную в двойные кавычки.

Цепочка литер в двойных кавычках типа "здравствуй, мир\n" называется стрингом литер или стринговой константой. Какое-то время в качестве аргументов для printf и других функций мы будем использовать только стринги литер.

В Си комбинация \n внутри стринга обозначает литеру новая_строка, которая при печати вызывает переход к левому краю следующей строки. Если вы удалите \n (стоит поэкспериментировать), то обнаружите, что, закончив печать, машина не переходит на новую строку. Литеру новая_строка в текстовый аргумент printf следует включать явным образом. Если вы попробуете выполнить, например,

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

компилятор выдаст сообщение об ошибке.

Литера новая_строка никогда не вставляется автоматически, так что одну строку можно напечатать по шагам с помощью нескольких обращений к printf. Нашу первую программу можно написать и так:

#include <stdio.h>
main()
{
    printf("здравствуй, ");
    printf("мир");
    printf("\n");
}

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

Заметим, что \n обозначает только одну литеру. Такие особые комбинации литер, начинающиеся с обратной наклонной черты, как \n, и называемые эскейп-последовательностями, широко применяются для обозначения трудно представимых или невидимых литер. Среди прочих в Си имеются литеры \t, \b, \", \\. обозначающие соответственно табуляцию, возврат на одну литеру назад («забой» последней литеры), двойную кавычку, саму наклонную черту. Полный список таких литер представлен в разд. 2.3.

Упражнение 1.1.

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

Упражнение 1.2.

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

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

Следующая программа выполняет вычисления по формуле C° = (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 — универсальная функция форматного ввода-вывода, которая будет подробно описана в гл. 7. Ее первый аргумент — стринг, в котором каждый знак % соответствует одному из последующих ее аргументов (второму, третьему...), а информация, расположенная за знаком %, указывает на вид, в котором каждый из этих аргументов выводится. Например, %d специфицирует выдачу аргумента в виде целого числа, и инструкция

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

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

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

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

Желая сконцентрировать ваше внимание на самом Си, мы не будем много говорить о вводе-выводе до гл. 7. В частности, до этой главы мы отложим разговор о форматном вводе. Если вам потребуется ввести числа, советуем прочитать в разд. 7.4 то, что касается функции 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°C, а не -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 <= upper) {
        celsius = (5.0 / 9.0) * (fahr - 32.0);
        printf("%3.0f %6.1f\n", fahr, celsius);
        fahr = fahr + step;
    }
}

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

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

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 печать десятичного целого в поле из 6 позиций
%f печать числа с плавающей точкой
%6f печать числа с плавающей точкой в поле из 6 позиций
%.2f печать числа с плавающей точкой с 2 цифрами после десятичной точки
%6.2f печать числа с плавающей точкой в поле из 6 позиций и 2 цифрами после десятичной точки

Кроме того, printf допускает также следующие спецификаторы: %o для восьмеричного числа, %x для шестнадцатиричного числа, %c для литеры, %s для стринга литер и %% для самого %.

Упражнение 1.3.

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

Упражнение 1.4.

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

1.3. Инструкция for

Существует много разных способов для написания одной и той же программы. Видоизменим нашу программу преобразования температур:

#include <stdio.h>

/* печать таблицы температур по Фаренгейту и Цельсию */
main()
{
    int fahr;

    for (fahr = 0; fahr <= 300; fahr = fahr + 20)
        printf("%3d% 6.1f\n", fahr, (5.0 / 9.0) * (fahr - 32));
}

Эта программа печатает тот же результат, но выглядит она, несомненно, по-другому. Главное отличие в отсутствии большинства переменных. Осталась только переменная fahr, которую мы описали как int. Нижняя и верхняя границы и шаг присутствуют в виде констант в инструкции for — новой для нас конструкции, а выражение, вычисляющее температуру по Цельсию, теперь задано третьим аргументом функции printf, а не в отдельной инструкции присваивания.

Это последнее изменение является примером применения общего правила: в любом контексте, где возможно использование значения переменной какого-то типа, можно использовать более сложное выражение того же типа. Так, на месте третьего аргумента функции printf согласно спецификатору %6.1f должно быть значение с плавающей точкой, следовательно, здесь может быть любое выражение этого типа.

Инструкция for описывает цикл, который является обобщением цикла while. Если вы сравните его с ранее написанным while, то вам станет ясно, как он работает. Внутри скобок имеются три выражения, разделяемых точкой с запятой. Первое выражение — инициализация

fahr = 0

— выполняется один раз перед тем, как войти в цикл. Второе — проверка условия продолжения цикла

fahr <= 300

Условие вычисляется, и, если оно истинно, тело цикла (в нашем случае это одно обращение к printf) выполняется. Затем осуществляется приращение шага:

fahr = fahr + 20

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

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

Упражнение 1.5.

Измените программу преобразования температур так, чтобы она печатала таблицу в обратном порядке, т.е. от 300 до 0.

1.4. Именованные константы

Прежде чем мы закончим рассмотрение программы преобразования температур, выскажем еще одно соображение. Очень плохо, когда по программе рассеяны «магические числа» типа 300, 20. Для того кто будет читать программу, в них нет и намека на то, что они собой представляют. Кроме того, их трудно заменить на другие каким-то систематическим способом. Одна из возможностей справиться с такими числами — дать им осмысленные имена. Строка #define определяет символьное идя, или именованную константу, для заданного стринга литер:

#define имя подставляемый_текст

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

#include <stdio.h>

#define LOWER 0     /* нижняя граница таблицы */
#define UPPER 300   /* верхняя граница */
#define STEP 20     /* размер шага */

/* печать таблицы температур по Фаренгейту и Цельсию */
main()
{
    int fahr;

    for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP)
        printf("%3d% 6.1f\n", fahr, (5.0 / 9.0) * (fahr - 32));
}

Величины LOWER, UPPER и STEP — именованные константы, а не переменные. Поэтому для них нет описаний. По общепринятому соглашению имена именованных констант набираются заглавными буквами, чтобы они отличались от обычных переменных, набираемых строчными. Заметим, что в конце #define-строки точка с запятой не ставится.

1.5. Ввод–вывод литер

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

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

Стандартная библиотека включает несколько функций для чтения и записи одной литеры. Простейшие из них — getchar и putchar. За одно обращение к getchar читается следующая литера ввода из текстового потока, которая и выдается в качестве результата. Так, после выполнения

c = getchar();

переменная c содержит следующую литеру ввода. Обычно литеры поступают с клавиатуры. Ввод из файлов рассматривается в гл. 7. Обращение к putchar приводит к печати одной литеры. Так,

putchar(c);

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

1.5.1. Копирование файла

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

чтение литеры
while (литера не есть признак_конца_файла)
    вывод только что прочитанной литеры
    чтение литеры

Оформляя ее в виде программы на Си, получим

#include <stdio.h>

/* копирование ввода на вывод; 1-я версия */
main()
{
    int c;

    c = getchar();
    while (c != EOF) {
        putchar(c);
        c = getchar();
    }
}

Оператор отношения != означает «не равно».

Каждая литера, вводимая с клавиатуры или появляющаяся на экране, как и любая другая литера внутри машины, кодируется комбинацией бит. Тип char специально предназначен для хранения литерных данных, однако для этого также годится и любой целый тип. Мы пользуемся типом int и делаем это по одной важной причине, которая требует разъяснений.

Существует проблема: как отличить конец ввода от обычных читаемых данных. Решение в том, чтобы функция getchar по исчерпании входного потока выдавала в качестве результата такое значение, которое нельзя было бы спутать ни с одной реальной литерой. Это значение есть EOF (аббревиатура от end of file — конец файла). Мы должны объявить переменную c такого типа, чтобы его «хватило» для представления всех возможных результатов, выдаваемых функцией getchar. Нам не подходит тип char, так как c должна быть достаточно «емкой», чтобы помимо любого значения типа char быть в состоянии хранить и EOF. Вот почему мы используем int, а не char.

EOF — целая константа, определенная в <stdio.h>. Какое значение имеет эта константа — неважно, лишь бы оно отличалось от любого из возможных значений типа char. Использование именованной константы с унифицированным именем гарантирует, что программа не будет зависеть от конкретного числового значения, которое, возможно, в других Си-системах будет иным.

Программу копирования можно написать более сжато. В Си любое присваивание, например,

c = getchar();

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

#include <stdio.h>

/* копирование ввода на вывод; 2-я версия */
main()
{
    int c;

    while ((c = getchar()) != EOF)
        putchar(c);
}

Цикл while, пересылая в c полученное от getchar значение, сразу же проверяет: не является ли оно «концом файла». Если это не так, тело цикла while выполняется и литера печатается. По окончании ввода завершается работа цикла while и тем самым и main.

В данной версии ввод «централизован» — в программе имеется только одно обращение к getchar. В результате она более компактна и легче воспринимается при чтении. Вам часто придется сталкиваться с такой формой записи, где присваивание делается вместе с проверкой. (Чрезмерное увлечение ею, однако, может запутать программу, поэтому мы постараемся пользоваться указанной формой разумно.)

Скобки внутри условия, обрамляющие присваивание, необходимы. Приоритет != выше, чем приоритет =, из чего следует, что при отсутствии скобок проверка != будет выполняться до операции присваивания =. Таким образом, запись

c = getchar() != EOF

эквивалентна записи

c = (getchar() != EOF)

А это совсем не то, что нам нужно: переменной c будет присваиваться 0 или 1 в зависимости от того, встретит или не встретит getchar признак конца файла. (Более подробно об этом см. в гл. 2.)

Упражнение 1.6.

Убедитесь в том, что выражение getchar() != EOF получает значение 0 или 1.

Упражнение 1.7.

Напишите программу, печатающую значение EOF.

1.5.2. Подсчет литер

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

#include <stdio.h>

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

    nc = 0;
    while (getchar() != EOF) {
        ++nc;
    printf("%1d\n", nc);
}

Инструкция

++nc;

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

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

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

#include <stdio.h>

/* подсчет строк входного потока */
main()
{
    int c, nl;

    nl = 0;
    while ((c = getchar()) != EOF)
        if (c == '\n')
            ++nl;
    printf("%d\n", nl);
}

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

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

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

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

Упражнение 1.8.

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

Упражнение 1.9.

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

Упражнение 1.10.

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

1.5.4. Подсчет слов

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

#include <stdio.h>

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

/* подсчет строк, слов и литер */
main()
{
    int c, nl, nw, nc, state;

    state = OUT;
    nl = nw = nc = 0;
    while ((c = 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.

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

1.6. Массивы

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

#include <stdio.h>

/* подсчет цифр, пробельных и прочих литер */
main()
{
    int c, i, nwhite, nother;
    int ndigit[10]:

    nwhite = nother = 0;
    for (i = 0; i < 10; ++i)
        ndigit[i] = 0;
    while ((c = getchar()) != EOF)
        if (c >= '0' && c <= '9')
            ++ndigit[c - '0'];
        else if (c == || c == '\n' || c == '\t')
            ++nwhite;
        else
            ++nother;
    printf("цифры =");
    for (i = 0; i < 10; ++i)
        printf(" %d", ndigit[i]);
    printf(", пробелы = %d, прочие = %d\n", nwhite, nother);
}

Если пропустить текст этой программы в качестве ввода, то будет напечатан следующий результат:

цифры = 9 3 0 0 0 0 0 0 0 1, пробелы = 141, прочие = 347

Декларация

int ndigit[10];

определяет ndigit как массив из 10 значений типа int. В Си элементы массива всегда нумеруются, начиная с нуля, так что элементами этого массива будут ndigit[0], ndigit[1], ..., ndigit[9], что учитывается в for-циклах (при инициализации и печати массива).

Индексом может быть любое целое выражение, образуемое целыми переменными (например, i) и целыми константами.

Приведенная программа опирается на определенные свойства кодировки цифр. Например, проверка

if (c >= '0' && c <= '9')

определяет, является ли находящаяся в c литера цифрой. Если это так, то

c - '0'

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

По определению значения типа char являются всего лишь малыми целыми, так что переменные и константы типа char в арифметических выражениях идентичны значениям типа int. Это и естественно, и удобно; например, c-'0' есть целое выражение с возможными значениями от 0 до 9, которые соответствуют литерам от '0' до '9', хранящимся в переменной c. Таким образом, значение данного выражения является правильным индексом для массива ndigit.

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

if (c >= '0' && c <= '9')
    ++ndigit[c - '0'];
else if (c == || c == '\n' || c == '\t')
    ++nwhite;
else
    ++nother;

Конструкция вида

if (условие1)
    инструкция1
else if (условие2)
    инструкция2
else
    инструкция_n

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

Между первым if и завершающим else может быть сколько угодно комбинаций вида

else if (условие)
    инструкция

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

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

Упражнение 1.13.

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

Упражнение 1.14.

Напишите программу, печатающую гистограммы частот встречаемости вводимых литер.

1.7. Функции

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

До сих пор мы пользовались готовыми функциями типа printf, getchar и putchar, теперь настала пора написать несколько наших собственных функций. В Си нет оператора возведения в степень типа оператора ** в Фортране. Поэтому проиллюстрируем механизм определения функции на примере функции power(m, n), которая возводит целое m в целую положительную степень n. Так, power(2, 5) имеет значение 32. На самом деле для практического применения эта функция малопригодна, так как она оперирует лишь малыми целыми степенями, однако она достаточно хороша, чтобы послужить иллюстрацией. (В стандартной библиотеке есть функция pow(x, y), вычисляющая xy.)

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

#include <stdio.h>

int power(int m, int n);

/* тест функции power */
main()
{
    int i;

    for (i = 0; i < 10; ++i)
        printf("%d %d %d\n", i, power(2, i), power(-3, i));

    return 0;
}

/* возводит base в n-ю степень; n >= 0 */
int power(int base, int n)
{
    int i, p;

    p = 1;
    for (i = 1; i <= n; ++i)
        p = p * base;

    return p;
}

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

тип_результата имя_функции (список параметров, если он есть)
{
    декларации

    инструкции
}

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

В следующей строке из функции main к power обращаются дважды.

printf("%d %d %d\n", i, power(2, i), power(-3, i));

При каждом вызове функции power передаются два аргумента, и каждый раз главная программа в ответ получает целое число, которое затем приводится к должному формату и печатается. Как часть выражения power( 2, i) представляет собой целое значение точно так же, как 2 или i. (Не все функции в качестве результата выдают целые значения; подробно об этом будет сказано позже, в гл. 4.)

В первой строке определения power:

int power(int base, int n);

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

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

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

return выражение;

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

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

Декларация

int power(int m, int n);

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

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

int power(int, int);

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

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

/* power возводит base в n-ю степень; n >= 0    */
/* (версия в старом стиле языка Си)             */
power(base, n)
int base, n;
{
    int i, p;

    p = 1;
    for (i = 1; i <= n; ++i)
        p = p * base;

    return p;
}

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

Описание power в начале программы согласно первой версии Си должно было бы выглядеть следующим образом:

int power();

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

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

Упражнение 1.15.

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

1.8. Аргументы. Вызов по значению

Одно свойство функций в Си, вероятно, будет в новинку для программистов, которые уже пользовались другими языками и Фортраном в частности. В Си все аргументы функции передаются «по значению». Это следует понимать так, что вызываемой функции посылаются значения ее аргументов во временных переменных, а не сами аргументы. Такой способ передачи аргументов несколько отличается от «вызова по ссылке» в Фортране и спецификации var при параметре в Паскале, которые позволяют подпрограмме иметь доступ к самим аргументам, а не к их локальным копиям.

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

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

/* power возводит base в n-ю степень: n >= 0; версия 2 */
int power(int base, int n)
{
    int p;

    for (p = 1; n > 0; --n)
        p = p * base;

    return p;
}

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

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

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

1.9. Массивы литер

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

Ее схема достаточно проста:

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 && c != '\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, имеет целый тип. Задание размера массива в определении имеет целью резервирование памяти. В самой getline задавать длину массива s нет необходимости, так как его размер указан в main. Чтобы вернуть значение вызывающей программе, getline использует return точно так, как это делает функция power. В приведенной строке также сообщается, что getline возвращает значение типа int, но так как при отсутствии указания о типе подразумевается int, то перед getline слово int можно опустить.

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

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

"hello\n"

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

h e l l o \n \0

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

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

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

Упражнение 1.16.

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

Упражнение 1.17.

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

Упражнение 1.18.

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

Упражнение 1.19.

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

1.10. Внешние переменные и область действия

Переменные line, longest и т.д. принадлежат только функции main, или, как говорят, локализованы в ней. Поскольку они определены внутри main, никакие другие функции прямо к ним обращаться не могут. То же верно и применительно к переменным других функций; например, i в getline не имеет никакого отношения к i в copy. Каждая локальная переменная функции возникает только в момент обращения к этой функции и исчезает после выхода из нее. Вот почему такие переменные, следуя терминологии других языков, называют автоматическими. (В гл. 4 обсуждается класс памяти 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 нет необходимости. Описания extern. таким образом, в main, getline и copy избыточны. Обычно определения внешних переменных располагают в начале исходного файла, и все описания extern для них опускают.

Если же программа расположена в нескольких исходных файлах и внешняя переменная определена в файле1, а используется в файле2 и файле3, то extern-описания в файле2 и файле3 обязательны, поскольку необходимо указать, что во всех трех файлах функции ссылаются на одну и ту же внешнюю переменную. На практике обычно удобно собрать все описания внешних переменных и функций в отдельный файл, называемый головным (header-файлом), и помещать его с помощью #include в начало каждого исходного файла. Суффикс .h по общей договоренности используется для имен header-файлов. В головных файлах, в <stdio.h> в частности, описываются также функции стандартной библиотеки. Более подробно о головных файлах говорится в гл. 4, а применительно к стандартной библиотеке — в гл. 7 и приложении B.

Так как специализированные версии getline и copy не имеют аргументов, на первый взгляд кажется, что логично их прототипы задать в виде getline() и copy(). Но из соображений совместимости со старыми Си-программами стандарт рассматривает пустой список как сигнал к тому, чтобы выключить все проверки на соответствие аргументов. Поэтому, когда нужно сохранить контроль и явно указать отсутствие аргументов, следует пользоваться словом void. Мы вернемся к этой проблеме в гл. 4.

Заметим, что по отношению к внешним переменным в этом разделе мы очень аккуратно используем понятия определение и описание. «Определение» располагается в месте, где переменная создается и ей отводится память; «описание» помещается там, где фиксируется природа переменной, но никакой памяти для нее не отводится.

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

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

Упражнение 1.20.

Напишите программу detab, заменяющую литеры табуляции вводимого текста нужным числом пробелов (до следующего «стопа» табуляции). Предполагается, что «стопы» табуляции расставлены на фиксированном расстоянии друг от друга, скажем, через n позиций. Как лучше задавать n — в виде значения переменной или в виде именованной константы?

Упражнение 1.21.

Напишите программу entab, заменяющую цепочки пробелов минимальным числом литер табуляций и пробелов таким образом, чтобы вид напечатанного текста не изменился. Используйте те же «стопы» табуляции, что и в detab. В случае, когда для выхода на очередной «стоп» годится один пробел, что лучше — пробел или литера табуляции?

Упражнение 1.22.

Напишите программу, печатающую литеры входного потока так, чтобы строки текста не выходили правее n-й позиции. Это значит, что каждая строка, длина которой превышает n, должна печататься с переносом на следующие строки. Место переноса следует «искать» после последней непробельной литеры, расположенной левее n-й позиции. Позаботьтесь о том, чтобы ваша программа вела себя разумно в случае очень длинных строк, а также, когда до n-й позиции не встречается ни одной литеры пробела или табуляции.

Упражнение 1.23.

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

Упражнение 1.24.

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