Kernighan, B. W. and Ritchie, D. M. "The 'C' Programming Language"; Chapter 1

Учебное введение

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

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

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

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

Содержание

1.1. Нaчинаем
1.2. Переменные и арифметика
1.3. Оператор for
1.4. Символьные константы
1.5. Набор полезных программ
1.5.1. Ввод и вывод символов
1.5.2. Копирование файла
1.5.3. Подсчет символов
1.5.4. Подсчет строк
1.5.5. Подсчет слов
1.6. Массивы
1.7. Функции.
1.8. Аргументы - вызов по значению.
1.9. Массивы символов.
1.10. Область действия: внешние переменные.
1.11. Резюме


Нaчинаем

Eдинственный способ освоить новый языкпрограммирования - писать на нем программы. Перваяпрограмма, которая должна быть написана, - одна для всехязыков:

Напечатать слова:

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

Программа печати "hello, world" на языке "C" имеетвид:

main(){	printf("hello, world\n");}

Как пропустить эту программу - зависит отиспользуемой вами системы. В частности, на операционнойсистеме "UNIX" вы должны завести исходную программу вфайле, имя которого оканчивается на ".c", например,hello.c, и затем скомпилировать ее по команде

cc hello.c

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

a.out
приведет к выводу
hello, world

На других системах эти правила будут иными;проконсультируйтесь с местным авторитетом.

Упражнение 1-1.
Пропустите эту программу на вашей системе.Попробуйте не включать различные части программы ипосмотрите какие сообщения об ошибках вы при этомполучите.

Теперь некоторые пояснения к самой программе. Любая"C"-программа, каков бы ни был ее размер, состоит изодной или более "функций", указывающих фактическиеоперации компьютера, которые должны быть выполнены.Функции в языке "C" подобны функциям и подпрограммамфортрана и процедурам pl/1, паскаля и т.д. в нашемпримере такой функцией является main. Обычно вы можетедавать функциям любые имена по вашему усмотрению, но main- это особое имя;выполнение вашей программы начинаетсясначала с функции main. Это означает, что каждаяпрограмма должна в каком-то месте содержать функцию сименем main. Для выполнения определенных действийфункция main обычно обращается к другим функциям, частьиз которых находится в той же самой программе, а часть -в библиотеках, содержащих ранее написанные функции.

Одним способом обмена данными между функциямиявляется передача посредством аргументов. Круглые скобки,следующие за именем функции, заключают в себе списокаргументов;здесь маin - функция без аргументов, чтоуказывается как (). Операторы, составляющие функцию,заключаются в фигурные скобки '{' и '}',которые аналогичныDO-END в pl/1 или begin-end в алголе, паскале и т.д.Обращение к функции осуществляется указанием ее имени, закоторым следует заключенный в круглые скобки списокаргументов. Здесь нет никаких операторов call, как вфортране или pl/1. круглые скобки должны присутствовать ив том случае, когда функция не имеет аргументов.

Строка

printf("hello, world\n");
является обращением к функции, которое вызывает функцию сименем printf и аргуметом "hello, world\n". Функцияprintf является библиотечной функцией, которая выдаетвыходные данные на терминал (если только не указанокакоe-то другое место назначения). В данном случаепечатается строка символов, являющаяся аргументомфункции.

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

Последовательность \n в приведенной строке являетсяобозначением на языке "C" для 'символа новой строки',который служит указанием для перехода на терминале клевому краю следующей строки. Если вы не включите \n(полезный эксперимент), то обнаружите, что ваша выдача незакончится переходом терминала на новую строку.Использование последовательности \n - единственный способвведения символа новой строки в аргумент функции printf ;если вы попробуете что-нибудь вроде:

printf("hello, world  ");
тo "C"-компилятор будет печатать злорадныедиагностические сообщения о недостающих кавычках.

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

main(){	printf("hello, ");	printf("world");	printf("\n");}

Подчеркнем, что \n представляет только один символ.Условные 'последовательности', подобные \n, дают общий идопускающий расширение механизм для представления трудныхдля печати или невидимых символов. Среди прочих символовв языке "C" предусмотрены следующие:

\t - для табуляции,
\b - для возврата на одну позицию, " - для двойнойкавычки и \\ для самой обратной косой черты.
Упражнение 1-2.
Проведите эксперименты для того, чтобы узнать чтопроизойдет, если в строке, являющейся аргументом функцииprintf будет содержаться \x, где x - некоторый символ, невходящий в вышеприведенный список.


Переменные и арифметика

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

c = (5/9)*(f-32).0 -17.820 -6.740 4.460 15.6... ...260 126.7280 137.8300 140.9
Теперь сама программа:
/* * print fahrenheit-celsius table for f = 0, 20, * ..., 300 */main(){	int             lower, upper, step;	float           fahr, celsius;	lower = 0;		/* lower limit of				 * temperature table */	upper = 300;		/* upper limit */	step = 20;		/* step size */	fahr = lower;	while (fahr <= upper) {		celsius = (5.0 / 9.0) * (fahr - 32.0);		printf("%4.0f %6.1f\n", fahr, celsius);		fahr = fahr + step;	}}
Первые строки
/* * print fahrenheit-celsius table for f = 0, 20, * ..., 300 */
являются комментарием, который в данном случае краткопоясняет, что делает программа. Любые символы между /* и*/ игнорируются компилятором;можно свободно пользоватьсякомментариями для облегчения понимания программы.Комментарии могут появляться в любом месте, где возможенпробел или переход на новую строку.

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

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

Тип int oзначает, что все переменные списка целые;тип float предназначен для чисел с плавающей точкой, т.е.для чисел, которые могут иметь дробную часть. Точностькак int, tak и float зависит от конкретной машины, накоторой вы работаете. На pdp-11, например, тип intсоответствует 16-битовому числу со знаком, т.е. числу,лежащему между -32768 и +32767. Число типа float - это32-битовое число, имеющее около семи значащих цифр илежащее в диапазоне от 10e-38 до 10e+38. В главе 2приводится список размеров для других машин.

В языке "C" предусмотрено несколько других основныхтипов данных, кроме int и float:

charсимвол - один байт
shortкороткое целое
longдлинное целое
doubleплавающее с двойной точностью

Размеры этих об'ектов тоже машинно-независимы;детали приведены в главе 2. Имеются также массивы,структуры и об'единения этих основных типов, указатели наних и функции, которые их возвращают;со всеми ними мывстретимся в свое время.

Фактически вычисления в программе переводатемператур начинаются с операторов присваивания

lower = 0;upper = 300;step = 20;fahr = lower;
которые придают переменным их начальные значения. Каждыйотдельный оператор заканчивается точкой с запятой.

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

while (fahr <= upper) {	....}
проверяется условие в круглых скобках. Eсли оно истинно(fahr меньше или равно upper), то выполняется тело цикла(все операторы, заключенные в фигурные скобки).Затем вновь проверяется это условие и, если оно истинно,опять выполняется тело цикла. Если же условие невыполняется ( fahr превосходит upper ), циклзаканчивается и происходит переход к выполнениюоператора, следующего за оператором цикла. Так как внастоящей программе нет никаких последующих операторов,то выполнение программы завершается.

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

while (i < j)	i = 2 * i;
В обоих случаях операторы, управляемые оператором while,сдвинуты на одну табуляцию, чтобы вы могли с первоговзгляда видеть, какие операторы находятся внутри цикла.Tакой сдвиг подчеркивает логическую структуру программы.Хотя в языке "C" допускается совершенно произвольноерасположение операторов в строке, подходящий сдвиг ииспользование пробелов значительно облегчают чтениепрограмм. Мы рекомендуем писать только один оператор настроке и (обычно) оставлять пробелы вокруг операторов.Расположение фигурных скобок менее существенно;мывыбрали один из нескольких популярных стилей. Выберитеподходящий для вас стиль и затем используйте егопоследовательно.

Основная часть работы выполняется в теле цикла.температура по Цельсию вычисляется и присваиваетсяпеременной celsius оператором

celsius = (5.0 / 9.0) * (fahr - 32.0);

Причина использования выражения 5.0/9.0 вместовыглядящего проще 5/9 заключается в том, что в языке "C",как и во многих других языках, при делении целыхпроисходит усечение, состоящее в отбрасывании дробнойчасти результата. Таким образом, результат операции 5/9равен нулю, и, конечно, в этом случае все температурыоказались бы равными нулю. Десятичная точка в константеуказывает, что она имеет тип с плавающей точкой, так что,как мы и хотели, 5.0/9.0 равно 0.5555... .

Мы также писали 32.0 вместо 32, несмотря на то, чтотак как переменная fahr имеет тип float, целое 32автоматически бы преобразовалось к типу float ( в 32.0)перед вычитанием. С точки зрения стиля разумно писатьплавающие константы с явной десятичной точкой даже тогда,когда они имеют целые значения;это подчеркивает ихплавающую природу для просматривающего программу иобеспечивает то, что компилятор будет смотреть на вещитак же, как и вы.

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

fahr = lower;
и проверка
while (fahr <= upper)
работают, как ожидается, - перед выполнением операцийцелые преобразуются в плавающую форму.

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

printf("%4.0f %6.1f\n", fahr, celsius);
спецификация преобразования %4.0f говорит, что число сплавающей точкой должно быть напечатано в поле шириной покрайней мере в четыре символа без цифр после десятичнойточки. Спецификация %6.1f описывает другое число, котороедолжно занимать по крайней мере шесть позиций с однойцифрой после десятичной точки, аналогично спецификациямf6.1 в фортране или f(6,1) в pl/1. Различные частиспецификации могут быть опущены:спецификация %6fговорит, что число будет шириной по крайней мере в шестьсимволов;спецификация %2 требует двух позиций последесятичной точки, но ширина при этом не ограничивается;спецификация %f говорит только о том, что нужнонапечатать число с плавающей точкой. Функция printf такжераспознает следующие спецификации:%d - для десятичногоцелого, %о - для восьмеричного числа, %х - дляшестнадцатиричного, %с - для символа, %s - для символьнойстроки и %% - для самого символа %.

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

Между прочим, функция printf не является частьюязыка "C";в самом языке "C" не определены операцииввода-вывода. Нет ничего таинственного и в функции printf;это - просто полезная функция, являющаяся частьюстандартной библиотеки подпрограмм, которая обычнодоступна "C"-программам. Чтобы сосредоточиться на самомязыке, мы не будем подробно останавливаться на операцияхввода-вывода до главы 7. В частности, мы до тех поротложим форматный ввод. Если вам надо ввести числа -прочитайте описание функции scanf вглаве 7, раздел 7.4.Функция scanf во многом сходна с printf, но онасчитывает входные данные, а не печатает выходные.

Упражнение 1-3.
Преобразуйте программу перевода температур таким образом,чтобы она печатала заголовок к таблице.

Упражнение 1-4.
Напишите программы печати соответствующей таблицыперехода от градусов Цельсия к градусам Фаренгейта.


Оператор for

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

main(){				/* fahrenheit-celsius				 * table */        int fahr;	for (fahr = 0; fahr <= 300; fahr = fahr + 20)		printf("%4d %6.1f\n",		       fahr, (5.0 / 9.0) * (fahr - 32.0));}

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

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

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

fahr = 0
выполняется один раз перед входом в сам цикл. Вторая часть - проверка,Aли условие, которое управляет циклом:
fahr <= 300

Это условие проверяется и, если оно истинно, то выполняется телоцикла (в данном случае только функция printf ). Затемвыполняется шаг реинициализации

fahr = fahr + 20
и условие проверяется снова. Цикл завершается, когда это условиестановится ложным. Так же, как и в случае оператора while,тело цикла может состоять из одного оператора или изгруппы операторов, заключенных в фигурные скобки. Инициализирующая иреинициализирующая части могут быть любыми отдельными выражениями.

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

Упражнение 1-5
Модифицируйте программу перевода температур таким образом, чтобыона печатала таблицу в обратном порядке, т.е. от 300 градусовдо 0.


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

Последнее замечание, прежде чем мы навсегда оставим программу переводатемператур. Прятать "магические числа", такие как 300 и 20,внутрь программы - это неудачная практика;они дают мало информации тем,кто, возможно, должен будет разбираться в этой программе позднее,и их трудно изменять систематическим образом. К счастью вязыке "C" предусмотрен способ, позволяющий избежать таких"магических чисел". Используя конструкцию #define, вы можетев начале программы определить символическое имя илисимволическую константу, которая будет конкретной строкой символов.Впоследствии компилятор заменит все не заключенные в кавычкипоявления этого имени на соответствующую строку. Фактическиэто имя может бытьзаменено абсолютно произвольным текстом, не обязательноцифрами

#define lower 0			/* lower limit of table */#define upper 300		/* upper limit */#define step 20			/* step size */main(){				/* fahrenheit-celsius				 * table */        int fahr;	for (fahr = lower; fahr <= upper; fahr = fahr + step)		printf("%4d %6.1f\n",		fahr, (5.0 / 9.0) * (fahr - 32));}

Величины lower, upper и step являются константами и поэтомуони не указываются в описаниях. Символические имена обычно пишутпрописными буквами, чтобы их было легко отличить от написанныхстрочными буквами имен переменных. Отметим, что в конце определенияне ставится точка с запятой. Так как подставляется вся строка,следующая за определенным именем, то это привело бы к слишкомбольшому числу точек с запятой в операторе for .


Набор полезных программ

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

Ввод и вывод символов

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

c = getchar();

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

Функция putchar(с) является дополнением к getchar :в результате обращения

putchar (с);
содержимое переменной 'с' выдается на некоторый выходной носитель,обычно опять на терминал. Обращение к функциям putchar и printfмогут перемежаться;выдача будет появляться в том порядке, в которомпроисходят обращения.

Как и функция printf, функции getchar и putchar несодержат ничего экстраординарного. Они не входят в состав языка "C",но к ним всегда можно обратиться.

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

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

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

Программа, написанная на языке "Си", выглядит следующим образом:

#include <stdio.h>main(){				/* copy input tо output;				 * 1st version */        int c;	c = getchar();	while (c != EOF) {		putchar(c);		c = getchar();	}}
Оператор отношения != означает "не равно".

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

#define EOF -1
либо
#define EOF 0

Использование символической константы EOF для представлениязначения, возвращаемого функцией getchar при выходе на конецфайла, гарантирует, что только одна величина в программезависит от конкретного численного значения.

Мы также описали переменную 'c' как int, а не char, с темчтобы она могла хранить значение, возвращаемое getchar . Как мыувидим в главе 2,эта величина действительно int, так как она должнабыть в состоянии в дополнение ко всем возможным символам представлятьи EOF .

Программистом, имеющим опыт работы на "C", программа копирования была бынаписана более сжато. В языке "C" любое присваивание,такое как

c = getchar();

может быть использовано в выражении;его значение - простозначение, присваиваемое левой части. Еслиприсваивание символа переменной 'с' поместить внутрь проверочнойчасти оператора while, то программа копирования файла запишетсяв виде:

main(){				/* copy input то output;				 * 2nd version */        int c;	while ((c = getchar()) != EOF)		putchar(c);}

Программа извлекает символ, присваивает его переменной 'c'и затем проверяет, не является ли этот символ признакомконца файла. Если нет - выполняется тело оператора while,выводящее этот символ. Затем цикл while повторяется. Когда,наконец, будет достигнут конец файла ввода, оператор while завершается,а вместе с ним заканчивается выполнение и функции main.

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

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

c = getchar() != EOF
эквивалентен оператору
c = (getchar() != EOF)

Это, вопреки нашему желанию, приведет к тому, что 'c' будет приниматьзначение 0 или 1 в зависимости от того, натолкнется или нетgetchar на признак конца файла.(Подробнее об этом будет сказано в главе 2.)"

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

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

main(){				/* count characters in				 * input */	long            nc;	nc = 0;	while (getchar() != EOF)		++nc;	printf("%1d\n", nc);}
Оператор
++nc;
демонстрирует новую операцию, ++, которая означает увеличение наединицу. Вы могли бы написать nc = nc + 1, но ++nc болеекратко и зачастую более эффективно. Имеется соответствующая операция-- уменьшение на единицу. Операции ++ и -- могут быть либо префиксными(++nc), либо постфиксными (nc++);эти две формы, как будет показанов главе 2,имеют в выражениях различные значения, но как ++nc, таки nc++ увеличивают nc. Пока мы будем придерживатьсяпрефиксных операций.

Программа подсчета символов накапливает их количество в переменнойтипа long, а не int . На pdp-11 максимальное значениеравно 32767, и если описать счетчик как int, то он будетпереполняться даже при сравнительно малом файле ввода;на языке "C"для honeywell и ibm типы long и int являются синонимами и имеютзначительно больший размер. Спецификация преобразования %1dуказывает printf, что соответствующий аргумент является целымтипа long .

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

main(){				/* count characters in				 * input */	double          nc;	for (nc = 0; getchar() != EOF; ++nc);	printf("%.0f\n", nc);}

Функция printf использует спецификацию %f как для float,так и для double;спецификация %.0f подавляет печатьнесуществующей дробной части.

Тело оператора цикла for здесь пусто, так как вся работавыполняется в проверочной и реинициализационной частях.Но грамматические правила языка "C" требуют, чтобы оператор for имелтело. Изолированная точка с запятой, соответствуюшая пустомуоператору, появляется здесь, чтобы удовлетворить этомутребованию. Мы выделили ее на отдельную строку, чтобысделать ее более заметной.

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

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

Следующая программа подсчитывает количество строк в файле ввода.Предполагается, что строки ввода заканчиваются символом новой строки\n, скрупулезно добавленным к каждой выписанной строке.

main(){				/* count lines in input */	int            с, nl;	nl = 0;	while ((с= getchar()) != EOF)		if (с== '\n')			++nl;	printf("%d\n", nl);}

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

Удвоенный знак равенства == является обозначением в языке "C" для"равно" (аналогично .eq. в фортране). Этот символ введен для того, чтобыотличать проверку на равенство от одиночного =, используемого приприсваивании. Поскольку в типичных "C"-программах знак присваиваниявстречается примерно в два раза чаще, чем проверка на равенство, тоестественно, чтобы знак оператора был вполовину короче.

Любой отдельный символ может быть записан внутри одиночных кавычек, ипри этом ему соответствует значение, равное численному значению этогосимвола в машинном наборе символов;это называется символьнойконстантой. Так, например, 'a' - символьная константа;ее значениев наборе символов ascii (американский стандартный код для обменаинформацией) равно 65, внутреннему представлению символа a. Конечно,'a' предпочтительнее, чем 65:его смысл очевиден и он не зависит отконкретного машинного набора символов.

Условные последовательности, используемые в символьных строках, такжезанимают законное место среди символьных констант. Так в проверкахи арифметических выражениях '\n' представляет значение символа новойстроки. Вы должны твердо уяснить, что '\n' - отдельный символ, которыйв выражениях эквивалентен одиночному целому;с другой стороны "\n" - этосимвольная строка, которая содержит только один символ. Вопрос осопоставлении строк и символов обсуждается в главе 2.

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

Упражнение 1-7.
Напишите программу, которая копирует ввод на вывод,заменяя при этом каждую последовательность изодного или более пробелов на один пробел.

Подсчет слов

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

#define yes 1#define no 0main(){				/* count lines, words,				 * chars in input */	int             c, nl, nw, inword;	inword = no;	nl = nw = nc = 0;	while ((c = getchar()) != EOF) {		++nc;		if (с== '\n')			++nl;		if (c == ' ' || c == '\n' || c == '\t')			inword = no;		else if (inword == no) {			inword = yes;			++nw;		}	}	printf("%d %d %d\n", nl, nw, nc);}

Каждый раз, когда программа встречает первый символ слова, онаувеличивает счетчик числа слов на единицу. Переменная inword следит затем, находится ли программа в настоящий момент внутри слова или нет;сначала этой переменной присваивается " не в слове", чему соответствуетзначение no. Мы предпочитаем символические константы yes и no литернымзначениям 1 и 0, потому что они делают программу более удобной длячтения. Конечно, в такой крошечной программе, как эта, это не приводитк заметной разнице, но в больших программах увеличение ясности вполнестоит тех скромных дополнительных усилий, которых требует следованиеэтому принципу с самого начала. Вы также обнаружите, что существенныеизменения гораздо легче вносить в те программы, где числа фигурируюттолько в качестве символьных констант.

Строка

nl = nw = nc = 0;
полагает все три переменные равными нулю. Это не особый случай, аследствие того обстоятельства, что оператору присваивания соответствуетнекоторое значение и присваивания проводятся последовательно справаналево. Таким образом, дело обстоит так, как если бы мы написали
nc = (n = (nw = 0));
операция || означает OR, так что строка
if (c == ' ' || c == '\n' || c == '\t')
говорит "если c - пробел, или c - символ новой строки, илиc -табуляция ...". (Условная последовательность \t являетсяизображением символа табуляции).

Имеется соответствующая операция && для AND. Выражения, связанныеоперациями && или ||, рассматриваются слева на право, и при этомгарантируется, что оценивание выражений будет прекращено, как толькостанет ясно, является ли все выражение истинным или ложным. Так, если'c' оказывается пробелом, то нет никакой необходимости проверять,является ли 'c' символом новой строки или табуляции, и такие проверкидействительно не делаются. В данном случае это не имеет принципиальногозначения, но, как мы скоро увидим, в более сложных ситуациях этаособенность языка весьма существенна.

Этот пример также демонстрирует операСор else языка "C", которыйуказывает то действие, которое должно выполняться, если условие,содержащееся в операторе if, окажется ложным. Общая форма такова:

if (выражение)	оператор-1else	оператор-2

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

Упражнение 1-9.
Как бы вы стали проверять программу подсчета слов ?Какие имеются ограничения ?

Упражнение 1-10.
Напишите программу, которая будет печатать слова из файлаввода, причем по одному на строку.

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


Массивы

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

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

main(){				/* count digits, white				 * space, others */	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' && с<='9')			++ndigit[c - '0'];		else if (c == ' ' || c == '\n' || c == '\t')			++nwhite;		else			++nother;	printf("digits =");	for (i = 0; i < 10; ++i)		printf(" %d", ndigit[i]);	printf("\nwhite space = %d, other = %d\n",	       nwhite, nother);}

Описание

int ndigit[10];
об'являет, что ndigit является массивом из десяти целых. В языке "C"индексы массива всегда начинаются с нуля /а не с 1, как в фортране илиpl/1/, так что элементами массива являются ndigit[0], ndigit[1],...,ndigit[9]. Эта особенность отражена в циклах for, которыеинициализируют и печатают массив.

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

Эта конкретная программа сильно опирается на свойства символьногопредставления цифр. Так, например, в программе проверка

if(c >= '0' && c <= '9')...
определяет, является ли символ в 'c' цифрой, и если это так, точисленное значение этой цифры определяется по формуле /c - '0'/.какой способ работает только в том случае, если значения символьныхконстант '0', '1' и т.д. положительны, расположены в порядкевозрастания и нет ничего, кроме цифр, между константами '0' и '9'.К счастью, это верно для всех общепринятых наборов символов.

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

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

if (c >= '0' && c <= '9')	++ndigit[c - '0'];else if (c == ' ' || c == '\n' || c == '\t')	++nwhite;else	++nother;
Конструкции
if (условие)	операторelse if (условие)	операторelse	оператор
часто встречаются в программах как средство выраженияситуаций, в которых осуществляется выбор одного изнескольких возможных решений.

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

else if (условие)	оператор

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

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

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


Функции.

В языке "C" функции эквивалентны подпрограммам или функциям в фортранеили процедурам в pl/1, паскале и т.д. функции дают удобный способзаключения некоторой части вычислений в черный ящик, который вдальнейшем можно использовать, не интересуясь его внутреннимсодержанием. Использование функций является фактически единственнымспособом справиться с потенциальной сложностью больших программ. Еслифункции организованы должным образом, то можно игнорировать то, какделается работа;достаточно знание того, что делается. Язык "C"разработан таким образом, чтобы сделать использование функций легким,удобным и эффективным. Вам будут часто встречаться функции длиной всегов несколько строчек, вызываемые только один раз, и они используютсятолько потому, что это проясняет некоторую часть программы.

До сих пор мы использовали только предоставленные нам функции типаprintf, getchar и putchar;теперь пора написать несколько нашихсобственных. Так как в "C" нет операции возведения в степень, подобнойоперации ** в фортране или pl/1, давайте проиллюстрируем механикуопределения функции на примере функции power(м,n), возводящей целое м вцелую положительную степень n. Так значение power(2,5) равно 32.Конечно, эта функция не выполняет всей работы операции **, посколькуона действует только с положительными степенями небольших чисел, нолучше не создавать дополнительных затруднений, смешивая несколькоразличных вопросов.

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

main(){				/* test power function */	int             i;	for (i = 0; i < 10; ++i)		printf("%d %d %d\n",		  i, power(2, i), power(-3, i));}power(x, n)			/* raise x n-th power; n				 * > 0 */	int             x, n;{	int             i, p;	p = 1;	for (i = 1; i <= n; ++i)		p = p * x;	return (p);}
Все функции имеют одинаковый вид:
имя (список аргументов, если они имеются)описание аргументов, если они имеются{	описания        операторы}

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

Функция power вызывается дважды в строке

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

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

Аргументы функции power должны быть описаны соответствующим образом, таккак их типы известны. Это сделано в строке

int x,n;
которая следует за именем функции.

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

Значение, вычисленное функцией power, передаются в main с помощьюоператора return, точно такого же, как в pl/1. Внутри круглых скобокможно написать любое выражение. Функция не обязана возвращать какоe-либозначение;оператор return, не содержащий никакого выражения, приводитк такой же передаче управления, как "сваливание на конец" функции придостижении конечной правой фигурной скобки, но при этом в вызывающуюфункцию не возвращается никакого полезного значения.

Упражнение 1-13.
Напишите программу преобразования прописных букв изфайла ввода в строчные, используя при этом функциюlower(c), которая возвращает значение 'c', если 'c'- не буква, и значение соответствующей строчнойбуквы, если 'c'-буква.


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

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

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

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

power(x, n)			/* raise x n-th power; n				 * > 0; version 2 */	int             x, n;{	int             p;	for (p = 1; n > 0; --n)		p = p * x;	return (p);}

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

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

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


Массивы символов.

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

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

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

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

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

Наконец, нам нужна основная программа для управления функциями getlineи copy . Вот результат :

#define maxline 1000		/* maximum input line				 * size */main(){				/* find longest line */	int             len;	/* current line length */	int             max;	/* maximum length seen				 * so far */	char            line[maxline];	/* current input line */	char            save[maxline];	/* longest line, saved */	max = 0;	while ((len = getline(line, maxline)) > 0)		if (len > max) {			max = len;			copy(line, save);		}	if (max > 0)		/* there was a line */		printf("%s", save);}getline(s, lim)			/* get line into				 * s,return length */	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(s1, s2)			/* copy s1 to s2; assume				 * s2 big enough */	char            s1[], s2[];{	int             i;	i = 0;	while ((s2[i] = s1[i]) != '\0')		++i;}

Функция main и getline общаются как через пару аргументов, так и черезвозвращаемое значение. Аргументы getline описаны в строках

char    s[];int     lim;
которые указывают, что первый аргумент является массивом,а второй - целым.

Длина массива s не указана, так как она определена в main .Функция getline использует оператор return для передачи значения назадв вызывающую программу точно так же, как это делала функция power.Одни функции возвращают некоторое нужное значение;другие, подобноcopy, используются из-за их действия и не возвращают никакого значения.

Чтобы пометить конец строки символов, функция getline помещает в конецсоздаваемого ей массива символ \0 /нулевой символ, значение которогоравно нулю/. Это соглашение используется также компилятором с языка "C":когда в "C" - программе встречается строчная константа типа

"hello\n"
то компилятор создает массив символов, содержащийсимволы этой строки, и заканчивает его символом \0, с темчтобы функции, подобные printf, могли зафиксировать конецмассива:

hello\n\0

Спецификация формата %s указывает, что printf ожидает строку,представленную в такой форме. Проанализировав функцию copy, выобнаружите, что и она опирается на тот факт, что ее входной аргументоканчивается символом \0, и копирует этот символ в выходной аргумент s2./Все это подразумевает, что символ \0 не является частью нормальноготекста/.

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

Пользователь функции getline никак не может заранее узнать, насколькодлинной окажется вводимая строка. Поэтому в getline включен контрольпереполнения. В то же время пользователь функции copy уже знает /илиможет узнать/, каков размер строк, так что мы предпочли не включать вэту функцию дополнительный контроль.

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

Упржнение 1-15.
Напишите программу печати всех строк длиннее 80 символов.

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

Упражнение 1-17.
Напишите функцию reverse(s), которая распологает символьную строку s вобратном порядке. С ее помощью напишите программу, которая обратиткаждую строку из файла ввода.


Область действия: внешние переменные.

Переменные в main(line, save и т.д.) являются внутренними или локальнымипо отношению к функции main, потому что они описаны внутри main иникакая другая функция не имеет к ним прямого доступа. Это же верно иотносительно переменных в других функциях;например, переменная i вфункции getline никак не связана с i в copy. Каждая локальная переменнаясуществует только тогда, когда произошло обращение к соответствующейфункции, и исчезает, как только закончится выполнение этой функции.По этой причине такие переменные, следуя терминологии других языков,обычно называют автоматическими. Мы впредь будем использовать терминавтоматические при ссылке на эти динамические локальные переменные.(В главе 4обсуждается класс статической памяти, когда локальныепеременные все же оказываются в состоянии сохранить свои значениямежду обращениями к функциям).

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

В качестве альтернативы к автоматическим ппеременным можно определитьпеременные, которые будут внешними для всех функий, т.е. глобальнымипеременными, к которым может обращаться по имени любая функция, котораяпожелает это сделать. (этот механизм весьма сходен с "common" вфортране и "external" в pl/1). Так как внешние переменные доступнывсюду, их можно использовать вместо списка аргументов для передачиданных между функциями. Кроме того, поскольку внешние переменныесуществуют постоянно, а не появляются и исчезают вместе с вызываемымифункциями, они сохраняют свои значения и после того, как функции,присвоившие им эти значения, завершат свою работу.

Внешняя переменная должна быть определена вне всех функций;при этомей выделяется фактическое место в памяти. Такая переменная должна бытьтакже описана в каждой функции, которая собирается ее использовать;это можно сделать либо явным описанием extern, либо неявным поконтексту. Чтобы сделать обсуждение более конкретным, давайте перепишемпрограмму поиска самой длинной строки, сделав line, save и махвнешними переменными. Это потребует изменения описаний и тел всех трехфункций, а также обращений к ним.

#define maxline 1000		/* max. input line size */char            line[maxline];	/* input line */char            save[maxline];	/* longest line saved				 * here */int             max;		/* length of longest				 * line seen so far */main(){				/* find longest line;				 * specialized version */	int             len;	extern int      max;	extern char     save[];	max = 0;	while ((len = getline()) > 0)		if (len > max) {			max = len;			copy();		}	if (max > 0)		/* there was a line */		printf("%s", save);}getline(){				/* specialized version */	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(){				/* specialized version */	int             i;	extern char     line[], save[];	i = 0;	while ((save[i] = line[i]) != '\0')		++i;}

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

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

Если программа находится в нескольких исходных файлах, и некотораяпеременная определена, скажем в файле 1, а используется в файле 2,то чтобы связать эти два вхождения переменной, необходимо в файле 2использовать описание extern. Этот вопрос подробно обсуждается вглаве 4.

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

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

Упражнение 1-18.
Проверка в операторе for функции getline довольно неуклюжа. Перепишитепрограмму таким образом, чтобы сделать эту проверку более ясной, носохраните при этом то же самое поведение в конце файла и припереполнении буфера. Является ли это поведение самым разумным?


Резюме

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

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

Упражнение 1-19.
Напишите программу detab,которая заменяет табуляции во вводе на нужноечисло пробелов так, чтобы промежуток достигал следующей табуляционнойостановки. Предположите фиксированный набор табуляционных остановок,например, через каждые n позиций.

Упражнение 1-20.
Напишите программу entab,которая заменяет строки пробелов минимальнымчислом табуляций и пробелов, достигая при этом тех же самыхпромежутков. Используйте те же табуляционные остановки, как и вdetab.

Упражнение 1-21.
Напишите программу для "сгибания" длинных вводимых строк послепоследнего отличного от пробела символа, стоящего до столбца n ввода,где n - параметр. Убедитесь, что ваша программа делает что-то разумноес очень длинными строками и в случае, когда перед указанным столбцомнет ни табуляций, ни пробелов.

Упражнение 1-22.
Напишите программу удаления из "C"-программы всех комментариев. Незабывайте аккуратно обращаться с "закавыченными" строками исимвольными константами.

Упражнение 1-23.
Напишите программу проверки "Си"-программы на элементарныесинтаксические ошибки, такие как несоответствие круглых, квадратных ифигурных скобок. Не забудьте о кавычках, как одиночных, так идвойных, и о комментариях. (эта программа весьма слложна , если выбудете писать ее для самого общего случая).