Главная страница
Навигация по странице:

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

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

  • 1.5. Ввод-вывод символов

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

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

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

  • Язык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents


    Скачать 2.33 Mb.
    НазваниеЯзык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents
    Дата18.09.2022
    Размер2.33 Mb.
    Формат файлаpdf
    Имя файлаBrian_Kernighan_Dennis_Ritchie-The_C_Programming_Language-RU.pdf
    ТипДокументы
    #683263
    страница4 из 31
    1   2   3   4   5   6   7   8   9   ...   31
    Упражнение 1.3. Усовершенствуйте программу преобразования температур таким образом, чтобы над таблицей она печатала заголовок.
    Упражнение 1.4. Напишите программу, которая будет печатать таблицу соответствия температур по Цельсию температурам по Фаренгейту.
    1.3. Инструкция for
    Существует много разных способов для написания одной и той же программы. Видоизменим нашу программу преобразования температур:
    #include
    /* печать таблицы температур по Фаренгейту и Цельсию */ 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 и fог определяется соображениями ясности программы. Цикл for более удобен в тех случаях, когда инициализация и приращение шага логически связаны друг с другом общей переменной и выражаются единичными инструкциями, поскольку названный цикл компактнее цикла while
    , а его управляющие части сосредоточены в одном месте.
    Упражнение 1.5. Измените программу преобразования температур так, чтобы она печатала таблицу в обратном порядке, т. е. от 300 до 0.
    1.4. Именованные константы
    Прежде чем мы закончим рассмотрение программы преобразования температур, выскажем еще одно соображение. Очень плохо, когда по программе рассеяны "загадочные числа", такие как 300, 20. Тот, кто будет читать программу, не найдет в них и намека на то, что они собой представляют. Кроме того, их трудно заменить на другие каким-то систематическим способом. Одна из возможностей справиться с такими числами — дать им осмысленные имена. Строка
    #define определяет символьное имя, или именованную
    константу, для заданной строки символов:
    #define имя подставляемый-текст
    С этого момента при любом появлении имени (если только оно встречается не в тексте, заключенном в кавычки, и не является частью определения другого имени) оно будет заменяться на соответствующий ему
    подставляемый-текст. Имя имеет тот же вид, что и переменная: последовательность букв и цифр, начинающаяся с буквы. Подставляемый-текст может быть любой последовательностью символов, среди которых могут встречаться не только цифры.
    #include
    #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 считывается следующий символ ввода из текстового потока, и этот символ выдается в качестве результата. Так, после выполнения с = getchar() переменная c
    содержит очередной символ ввода. Обычно символы поступают с клавиатуры. Ввод из файлов рассматривается в главе 7.
    Обращение к putchar приводит к печати одного символа. Так, putchar (c) напечатает содержимое целой переменной c
    в виде символа (обычно на экране). Вызовы putchar и printf могут произвольным образом перемежаться. Вывод будет формироваться в том же порядке, что и обращения к этим функциям.
    1.5.1. Копирование файла
    При наличии функций getchar и putchar
    , ничего больше не зная о вводе-выводе, можно написать удивительно много полезных программ. Простейший пример — это программа, копирующая по одному символу с входного потока в выходной поток:
    чтение символа
    while (символ не является признаком конца файла)
    вывод только что прочитанного символа
    чтение символа
    Оформляя ее в виде программы на Си, получим
    #include

    /* копирование ввода на вывод; 1-я версия */ main()
    { int с; с = getchar(); while (с != EOF) { putchar (c); с = getchar();
    }
    }
    Оператор отношения
    !=
    означает "не равно".
    Каждый символ, вводимый с клавиатуры или появляющийся на экране, как и любой другой символ внутри машины, кодируется комбинацией битов. Тип char специально предназначен для хранения символьных данных, однако для этого также годится и любой целый тип. Мы пользуемся типом int и делаем это по одной важной причине, которая требует разъяснений.
    Существует проблема: как отличить конец ввода от обычных читаемых данных. Решение заключается в том, чтобы функция getchar по исчерпании входного потока выдавала в качестве результата такое значение, которое нельзя было бы спутать ни с одним реальным символом. Это значение есть
    EOF
    (аббревиатура от end
    of file — конец файла). Мы должны объявить переменную с такого типа, чтобы его "хватило" для представления всех возможных результатов, выдаваемых функцией getchar
    . Нам не подходит тип char
    , так как c
    должна быть достаточно "емкой", чтобы помимо любого значения типа char быть в состоянии хранить и EOF. Вот почему мы используем int
    , а не char
    EOF
    — целая константа, определенная в

    . Какое значение имеет эта константа — неважно, лишь бы оно отличалось от любого из возможных значений типа char
    . Использование именованной константы с унифицированным именем гарантирует, что программа не будет зависеть от конкретного числового значения, которое, возможно, в других Си-системах будет иным.
    Программу копирования можно написать более сжато. В Си любое присваивание, например с = getchar() трактуется как выражение со значением, равным значению левой части после присваивания. Это значит, что присваивание может встречаться внутри более сложного выражения. Если присваивание переменной c
    расположить в проверке условия цикла while
    , то программу копирования можно будет записать в следующем виде:
    #include
    /* копирование ввода на вывод; 2-я версия */ main ()
    { int с; while ((с = getchar()) != EOF) putchar (c);
    }
    Цикл while
    , пересылая в с полученное от getchar значение, сразу же проверяет: не является ли оно "концом файла". Если это не так, выполняется тело цикла while и печатается символ. По окончании ввода завершается работа цикла while
    , а тем самым и main

    В данной версии ввод "централизован" — в программе имеется только одно обращение к getchar
    . В результате она более компактна и легче воспринимается при чтении. Вам часто придется сталкиваться с такой формой записи, где присваивание делается вместе с проверкой. (Чрезмерное увлечение ею, однако, может запутать программу, поэтому мы постараемся пользоваться указанной формой разумно.)
    Скобки внутри условия, вокруг присваивания, необходимы. Приоритет
    !=
    выше, чем приоритет
    =
    , из чего следует, что при отсутствии скобок проверка
    !=
    будет выполняться до операции присваивания
    =
    . Таким образом, запись с = getchar() != EOF эквивалентна записи с = (getchar() != EOF)
    А это совсем не то, что нам нужно: переменной c
    будет присваиваться 0 или 1 в зависимости от того, встретит или не встретит getchar признак конца файла. (Более подробно об этом см. в главе 2.)
    Упражнение 1.6. Убедитесь в том, что выражение getchar() != EOF
    получает значение 0 или 1 .
    Упражнение 1.7. Напишите программу, печатающую значение
    EOF
    1.5.2. Подсчет символов
    Следующая программа занимается подсчетом символов; она имеет много сходных черт с программой копирования.
    #include
    /* подсчет вводимых символов; 1-я версия */ main ()
    { long nc; nc = 0; while (getchar() != EOF)
    ++nc; printf ("%ld\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
    /* подсчет вводимых символов; 2-я версия */ main()
    { double nc; for (nc = 0; getchar() != EOF; ++nc)
    ; printf ("%.0f\n", nc);
    }
    В printf спецификатор
    %f применяется как для float
    , так и для double
    ; спецификатор
    %.0f означает печать без десятичной точки и дробной части (последняя в нашем случае отсутствует).
    Тело указанного fоr
    -цикла пусто, поскольку кроме проверок и приращений счетчика делать ничего не нужно. Но правила грамматики Си требуют, чтобы for
    -цикл имел тело. Выполнение этого требования обеспечивает изолированная точка с запятой, называемая пустой инструкцией. Мы поставили точку с запятой на отдельной строке для большей наглядности.
    Наконец, заметим, что если ввод не содержит ни одного символа, то при первом же обращении к getchar условие в while или for не будет выполнено и программа выдаст нуль, что и будет правильным результатом. Это важно. Одно из привлекательных свойств циклов while и for состоит в том, что условие проверяется до того, как выполняется тело цикла. Если ничего делать не надо, то ничего делаться и не будет, пусть даже тело цикла не выполнится ни разу. Программа должна вести себя корректно и при нулевом количестве вводимых символов. Само устройство циклов while и for дает дополнительную уверенность в правильном поведении программы в случае граничных условий.
    1.5.3. Подсчет строк
    Следующая программа подсчитывает строки. Как упоминалось выше, стандартная библиотека обеспечивает такую модель ввода-вывода, при которой входной текстовый поток состоит из последовательности строк, каждая из которых заканчивается символом новой строки. Следовательно, подсчет строк сводится к подсчету числа символов новой строки.
    #include
    /* подсчет строк входного потока */ main()
    { int с, nl; nl = 0; while ((с = getchar()) != EOF) if (c == '\n')
    ++nl; printf ("%d\n", nl);
    }
    Тело цикла теперь образует инструкция if
    , под контролем которой находится увеличение счетчика nl на единицу. Инструкция if проверяет условие в скобках и, если оно истинно, выполняет следующую за ним
    инструкцию (или группу инструкций, заключенную в фигурные скобки). Мы опять делаем отступы в тексте программы, чтобы показать, что чем управляется.
    Двойной знак равенства в языке Си обозначает оператор "равно" (он аналогичен оператору
    =
    в Паскале и
    EQ.
    в Фортране). Удваивание знака
    =
    в операторе проверки на равенство сделано для того, чтобы отличить его от единичного
    =
    , используемого в Си для обозначения присваивания. Предупреждаем: начинающие программировать на Си иногда пишут
    =
    , а имеют в виду
    ==
    . Как мы увидим в главе 2, в этом случае результатом будет обычно вполне допустимое по форме выражение, на которое компилятор не выдаст никаких предупреждающих сообщений
    3
    Символ, заключенный в одиночные кавычки, представляет собой целое значение, равное коду этого символа
    (в кодировке, принятой на данной машине). Это так называемая символьная константа. Существует и другой способ для написания маленьких целых значений. Например,
    'А'
    есть символьная константа; в наборе символов ASCII ее значение равняется 65 — внутреннему представлению символа А. Конечно,
    'А'
    в роли константы предпочтительнее, чем 65, поскольку смысл первой записи более очевиден, и она не зависит от конкретного способа кодировки символов.
    Эскейп-последовательности, используемые в строковых константах, допускаются также и в символьных константах. Так,
    '\n'
    обозначает код символа новой строки, который в ASCII равен 10. Следует обратить особое внимание на то, что '\n'
    обозначает один символ (код которого в выражении рассматривается как целое значение), в то время как "\n"
    — строковая константа, в которой чисто случайно указан один символ.
    Более подробно различие между символьными и строковыми константами разбирается в главе 2.
    Упражнение 1.8. Напишите программу для подсчета пробелов, табуляций и новых строк.
    Упражнение 1.9. Напишите программу, копирующую символы ввода в выходной поток и заменяющую стоящие подряд пробелы на один пробел.
    Упражнение 1.10. Напишите программу, копирующую вводимые символы в выходной поток с заменой символа табуляции на
    \t
    , символа забоя на
    \b и каждой обратной наклонной черты на
    \\
    . Это сделает видимыми все символы табуляции и забоя.
    1.5.4. Подсчет слов
    Четвертая из нашей серии полезных программ подсчитывает строки, слова и символы, причем под словом здесь имеется в виду любая строка символов, не содержащая в себе пробелов, табуляций и символов новой строки. Эта программа является упрощенной версией программы wc системы UNIX.
    #include
    #define IN 1 /* внутри слова */
    #define OUT 0 /* вне слова */
    /* подсчет строк, слов и символов */ main ()
    { int с, nl, nw, nc, state; state = OUT; nl = nw = nc = 0; while ((c = getchar()) != EOF) {
    ++nc; if (c == '\n' )
    ++nl;
    3
    Современные компиляторы, как правило, выдают предупреждение о возможной ошибке. — Примеч. ред.
    if (c == " " || c == '\n' || с == '\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 = пс = 0; устанавливает все три переменные в нуль. Такая запись не является какой-то особой конструкцией и допустима потому, что присваивание есть выражение со своим собственным значением, а операции присваивания выполняются справа налево. Указанная строка эквивалентна nl = (nw = (nc = 0));
    Оператор
    ||
    означает ИЛИ, так что строка if (с == ' ' || с == '\n' || с == '\t') читается как "если с
    есть пробел, или с
    есть новая строка, или с
    есть табуляция". (Напомним, что видимая эскейп-последовательность
    \t обозначает символ табуляции.) Существует также оператор
    &&
    , означающий
    И. Его приоритет выше, чем приоритет
    ||
    . Выражения, связанные операторами
    &&
    или
    ||
    , вычисляются слева направо; при этом гарантируется, что вычисления сразу прервутся, как только будет установлена истинность или ложность условия. Если c
    есть пробел, то дальше проверять, является значение с символом новой строки или же табуляции, не нужно. В этом частном случае данный способ вычислений не столь важен, но он имеет значение в более сложных ситуациях, которые мы вскоре рассмотрим.
    В примере также встречается слово else
    , которое указывает на альтернативные действия, выполняемые в случае, когда условие, указанное в if
    , не является истинным. В общем виде условная инструкция записывается так: if (выражение)
    инструкция
    1
    else
    инструкция
    2
    В конструкции if
    - else выполняется одна и только одна из двух инструкций. Если выражение истинно, то выполняется инструкция
    1
    , если нет, то инструкция
    2
    . Каждая из этих двух инструкций представляет собой либо одну инструкцию, либо несколько, заключенных в фигурные скобки. В нашей программе после else стоит инструкция if
    , управляющая двумя такими инструкциями.

    1   2   3   4   5   6   7   8   9   ...   31


    написать администратору сайта