Как записывается инструкция которая формирует символьную строку заданного формата
Перейти к содержимому

Как записывается инструкция которая формирует символьную строку заданного формата

  • автор:

Ввод и вывод данных в программах на языке Python

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

В данный момент вы не можете посмотреть или раздать видеоурок ученикам

Чтобы получить доступ к этому и другим видеоурокам комплекта, вам нужно добавить его в личный кабинет.

Получите невероятные возможности

1. Откройте доступ ко всем видеоурокам комплекта.

2. Раздавайте видеоуроки в личные кабинеты ученикам.

3. Смотрите статистику просмотра видеоуроков учениками.
Получить доступ

Конспект урока «Ввод и вывод данных в программах на языке Python»

· Инструкции считывания данных с клавиатуры, их вывода на экран.

· Использование инструкций ввода/вывода в программах.

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

Начнём с ввода данных. Для того, чтобы дать возможность пользователю ввести данные, используется функция input без параметров. Эта функция возвращает значение, которое пользователь ввёл с клавиатуры в строку. Рассмотрим её запись. Все функции в языке Python записываются в составе инструкций. Для вызова функции записывается её имя, после которого в скобках следуют её параметры. Так как функция input не имеет параметров, после её имени должны следовать пустые скобки. Так как программа записывает данные в переменную, то результат работы этой функции присваивается некоторой переменной. Таким образом, для считывания значения переменной a с клавиатуры нужно записать инструкцию присваивания переменной a значения функции input ().

Для вывода данных из оперативной памяти компьютера на экран монитора используется инструкция print. Мы уже пользовались ей раньше. Вспомним, как она записывается. После служебного слова print в скобках следует список выводимых данных. Как и в любой другой операции, в инструкции вывода могут указываться литералы, переменные и выражения.

Итак, мы узнали инструкции, используемые для ввода и вывода данных, теперь давайте попробуем их использовать в программе. Напишем модуль, который принимает на ввод 2 целых числа и выводит на экран их сумму. Начнём написание модуля. Числа мы будем хранить в переменных a и b. Поэтому в начале модуля запишем инструкцию присваивания переменной a значения функции input (). Дальше мы запишем такую же инструкцию для переменной b. После этого мы вычислим значение суммы этих переменных и выведем его на экран с помощью функции print. Для этого, после служебного слова print, в скобках запишем выражение: a + b.

Сохраним модуль и запустим его на выполнение. Первое число зададим равным 35. После ввода числа необходимо нажать клавишу «Enter». Второе число зададим равным 42. Очевидно, что по нашему замыслу программа должна была вывести на экран число 77, но вместо этого она вывела 3542. Хотя на самом деле это не названное число, а символьная строка, состоящая из четырёх цифр.

Почему так произошло? Здесь нужно понимать, что пользователь, задавая данные с клавиатуры, вводит их в текстовой форме. То есть функция input возвращает данные типа str, а нам, по условию задачи, нужны целые числа, то есть данные типа int. Для того, чтобы эти данные получить, нам необходимо воспользоваться функцией преобразования типов. Она записывается по названию типа выходных данных. В нашем случае – это целые числа, то есть int. В качестве параметра функции задаются данные, которые необходимо преобразовать. Таким образом, чтобы с клавиатуры считать целочисленное значение в переменную a, нужно присвоить ей значение функции int от значения функции input ().

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

После этого сохраним модуль и запустим его на выполнение. Введём те же числа, которые мы вводили до этого: 35 и 42. В этот раз на экран выведено предполагаемое значение – 77. Программа работает правильно.

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

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

print (‘Программа, вычисляющая сумму двух целых чисел. Введите два числа.’)

print (a, ‘+’, b, ‘=’, a + b)

Сохраним модуль и запустим его на выполнение. Теперь перед вводом данных программа отображает на экране сообщение о том, для чего она предназначена и что должен ввести пользователь. Введём числа 3 и 2. Программа вывела сообщение о том, что 3 + 2 = 5. Теперь наша программа понятна для пользователя.

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

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

print (‘Программа, вычисляющая сумму двух целых чисел. Введите два числа.’)

print (a, ‘+’, b, ‘=’, a + b, sep = »)

Снова сохраним наш модуль и запустим его на выполнение. Введём числа 3 и 2. На этот раз результирующее выражение было выведено без пробелов.

При обработке вывода данных часто бывает полезным использование форматированного вывода. В этом случае можно выделить некоторое количество знаковых позиций для вывода каждого значения. Для этого используется функция «Формат», которая формирует символьную строку заданного формата. Рассмотрим, как она работает. Для этого в интерактивном режиме среды разработки присвоим переменным a, b и c соответственно значения 15, 141 и 3. Дальше запишем инструкцию print, в которой, в кавычках, сначала запишем строку, описывающую формат вывода. Формат вывода каждого отдельного значения указывается в отдельных фигурных скобках. Он начинается с двоеточия. Дальше для целых чисел следует единственное число – количество выделяемых знаковых позиций и английская буква Ди. Выделим по пять знаковых позиций для вывода каждого числа. После строки формата ставится точка и записывается служебное слово format, после которого в скобках указываются выводимые значения. Мы укажем значения переменных a, b и c. Программа вывела отступы перед значениями, так как незанятые знаковые позиции заполняются пробелами. Если же знаковых позиций не хватает, они дополняются автоматически.

Теперь изменим написанный нами модуль так, чтобы он рассчитывал частное двух чисел, причём необязательно целых. Сначала изменим первое поясняющее сообщение. Это будет программа, вычисляющая значение частного двух чисел. После этого изменим функции преобразования типа для инструкций ввода. Так как числа необязательно будут целыми, то вводимые значения нужно преобразовать в вещественный тип float. Запишем слово float вместо int. А также изменим последнюю инструкцию вывода. Заменим выводимый знак «+» знаком «/», а в выводимом выражении сложение заменим делением.

print (‘Программа, вычисляющая частное двух чисел. Введите два числа.’)

print (a, ‘/’, b, ‘=’, a / b, sep = »)

Запустим программу на выполнение. Так как вводимые числа вещественные, у них может быть дробная часть, которая при вводе отделяется от целой части точкой. Введём первое число, равное 0.01, а второе — 5000. В результате программа вывела вместо последнего числа сообщение: 2e-06. Это называется экспоненциальной формой представления числа. Она означает, что результат равен произведению 2 и 10 -6 .

Для вывода вещественных значений также можно использовать форматированный вывод. Применим форматированный вывод для последнего числа. В качестве строки формата, в кавычках, между фигурными скобками укажем двоеточие, после которого будет следовать два целых числа, разделённые точкой – общее количество выделяемых знаковых позиций и количество выводимых знаков после запятой. Укажем 10 знаковых позиций и 7 знаков после запятой. Дальше для вещественных чисел следует английская буква f. После кавычек поставим точку и напишем служебное слово format, после которого в скобках укажем выводимое значение.

print (‘Программа, вычисляющая частное двух чисел. Введите два числа.’)

Запустим наш модуль на выполнение. Снова зададим числа 0.01 и 5000. На этот раз программа вывела ответ не в экспоненциальной, а в обычной форме.

· Для считывания данных с клавиатуры предназначена функция input, которая возвращает символьную строку – то, что пользователь ввёл с клавиатуры, прежде чем нажал клавишу «Enter».

· Если введённые данные необходимо преобразовать в числа, то используются функции преобразования типов: int и float.

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

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

Основы программирования на языке Python. [2 ed.] 9785976544307

Python Beginner To Pro: Python Tutorial, File Handling, Python NumPy, Python Matplotlib, Python SciPy, Machine Learning, Python MySQL,Python MySQL, Python Reference, Module Reference, Python Examples

Table of contents :
osnovy_programmirovania_na_azyke_python_1-14
osnovy_programmirovania_na_azyke_python_15-28
osnovy_programmirovania_na_azyke_python_29-42
osnovy_programmirovania_na_azyke_python_43-56
osnovy_programmirovania_na_azyke_python_57-70
osnovy_programmirovania_na_azyke_python_71-84
osnovy_programmirovania_na_azyke_python_85-98
osnovy_programmirovania_na_azyke_python_99-112
osnovy_programmirovania_na_azyke_python_113-126
osnovy_programmirovania_na_azyke_python_127-135

Citation preview

Министерство науки и высшего образования Российской Федерации Орский гуманитарно-технологический институт (филиал) федерального государственного бюджетного образовательного учреждения высшего образования «Оренбургский государственный университет»

Зыкова Г. В., Попов А. С., Сапуглецева Т. Н.

ОСНОВЫ ПРОГРАММИРОВАНИЯ НА ЯЗЫКЕ PYTHON Учебно-методическое пособие 2-е издание, стереотипное

Москва Издательство «ФЛИНТА» 2020

УДК 004.43 ББК 32.973.2 З 96

Уткина Т. И., доктор педагогических наук, профессор кафедры математики, информатики и физики Орского гуманитарно-технологического института (филиала) ОГУ

Рецензенты: Голунова А. А., кандидат педагогических наук, доцент кафедры математики, информатики и физики Орского гуманитарно-технологического института (филиала) ОГУ; Пергунов В. В., кандидат физико-математических наук, доцент кафедры прикладной информатики и математики Орского филиала АОЧУ ВО «Московский финансово-юридический университет МФЮА»

Зыкова Г. В. З 96 Основы программирования на языке Python [Электронный ресурс]: учеб.-метод. пособие / Г. В. Зыкова, А. С. Попов, Т. Н. Сапуглецева ; науч ред. Г. В. Зыковой. – 2-е изд., стер. – Москва : ФЛИНТА, 2020. – 135 с. ISBN 978-5-9765-4430-7 Данное учебно-методическое пособие разработано для начального курса изучения языка программирования Python, включенного в последние годы в контрольноизмерительные материалы ЕГЭ по информатике и, соответственно, в школьный курс информатики и ИКТ. Пособие предназначено для бакалавров направления 44.03.01 Педагогическое образование профиль «Информатика и ИКТ», а также может быть использовано школьниками и студентами среднего профессионального образования.

© Зыкова Г.В., 2019 © Издательство «ФЛИНТА», 2020

Содержание Введение …………………………………………………………… Предисловие ………………………………………………………. 1 Типы данных. Преобразование типов данных ……………. 1.1 Основы работы с типами данных ………………………. 1.2 Числа ………………………………………………………. 1.3 Строки ……………………………………………………. 2 Преобразование типов данных в Python ……………………. 2.1 Преобразование числовых типов ………………………… 2.2 Преобразование строк ……………………………………. Контрольные вопросы …………………………………………….. Задачи для самостоятельной работы …………………………… 3 Ввод и вывод данных ………………………………………….. Контрольные вопросы …………………………………………….. Задачи для самостоятельной работы …………………………… 4 Арифметические операции …………………………………… 4.1 Сложение ………………………………………………….. 4.2 Унарные арифметические операции ……………………. 4.3 Работа с комплексными числами ………………………. 4.4 Битовые операции ………………………………………… 4.5 Базовые операции над строками …………………………. 4.6 Приоритет операций ……………………………………… Контрольные вопросы …………………………………………….. Задачи для самостоятельной работы …………………………… 5 Логические операции ………………………………………….. 5.1 Операторы сравнения …………………………………….. 5.2 Логические операторы ……………………………………. 5.3 Таблицы истинности ……………………………………… Контрольные вопросы …………………………………………….. Задачи для самостоятельной работы …………………………… 6 Условные операторы …………………………………………. 6.1 Простой условный оператор ……………………………. 6.2 Составной условный оператор …………………………. 6.3 Условная конструкция if …………………………………. 6.4 Конструкция if. else ……………………………………… 6.5 Команда elif ……………………………………………….. 6.6 Вложенные условные инструкции ………………………. 3

6 9 14 14 15 17 20 20 21 25 26 27 33 33 35 36 39 40 41 42 43 44 45 46 46 48 51 52 53 54 54 54 55 56 57 58

6.7 Тернарная условная операция …………………………… Контрольные вопросы …………………………………………….. Задачи для самостоятельной работы …………………………… 7 Циклы ……………………………………………………………. 7.1 Понятие цикла …………………………………………….. 7.2 Цикл «while» ………………………………………………. 7.3 Инструкции break и continue в цикле while ……………. 7.4 Вложенные циклы while ………………………………….. 7.5 Цикл «for» …………………………………………………. 7.6 Оператор следующего прохода continue в цикле for …… 7.7 Оператор прерывания цикла break в цикле for …………. 7.8 Инструкция проверки прерывания else в цикле for …….. 7.9 Вложенные циклы for …………………………………….. Контрольные вопросы …………………………………………….. Задачи для самостоятельной работы …………………………… 8 Списки …………………………………………………………… 8.1 Списки и их особенности ………………………………… 8.2 Операции над последовательностями …………………… 8.3 Вложенные списки ………………………………………. 8.4 Генераторы списков ………………………………………. 8.5 Срезы ………………………………………………………. 8.6 Стек и очереди …………………………………………….. 8.7 Методы split и join ………………………………………… Контрольные вопросы …………………………………………….. Задачи для самостоятельной работы …………………………… 9 Кортежи ………………………………………………………….. 9.1 Операции, поддерживаемые кортежем …………………. 9.2 Удаление кортежей ……………………………………….. 9.3 Список кортежей ………………………………………….. 9.4 Сортировка ………………………………………………… 9.5 Преобразование кортежа в список и обратно …………. 9.6 Преобразование в словарь ……………………………….. 9.7 Преобразование кортежей в одну строку ……………….. Контрольные вопросы …………………………………………….. Задачи для самостоятельной работы …………………………… 10 Словари ………………………………………………………… 10.1 Создание …………………………………………………. 10.2 Добавление элемента ……………………………………. 4

59 60 60 62 62 63 65 66 68 70 71 72 73 75 75 77 80 81 83 84 87 88 88 90 90 92 94 97 97 97 98 99 99 100 100 102 104 105

10.3 Объединение словарей ………………………………….. 10.4 Удаление элемента ……………………………………… 10.5 Получение размера ……………………………………… 10.6 Перебор словаря …………………………………………. 10.7 Поиск ……………………………………………………. 10.8 Сортировка ……………………………………………….. 10.9 Сравнение ………………………………………………. 10.10 Копирование ……………………………………………. 10.11 Очистка …………………………………………………. 10.12 Генератор словарей …………………………………….. 10.13 Конвертация в строку ………………………………….. 10.14 Вложенные словари ……………………………………. Контрольные вопросы …………………………………………….. Задачи для самостоятельной работы …………………………… 11 Множества ……………………………………………………. 11.1 Создание множества …………………………………….. 11.2 Изменение множества …………………………………… 11.3 Удаление элементов множества ………………………. 11.4 Основные операции с множествами …………………… 11.5 Множество в логическом контексте …………………… 11.6 Метод copy ……………………………………………….. 11.7 Frozenset ………………………………………………….. Контрольные вопросы …………………………………………….. Задачи для самостоятельной работы …………………………… 12 Функции ………………………………………………………. 12.1 Создание функций ………………………………………. 12.2 Работа с функциями …………………………………….. 12.3 lambda-функции …………………………………………. 12.4 Аргументы функции в Python ………………………….. 12.5 Область видимости и глобальные переменные ……….. 12.6 Математические функции в Python ……………………. Контрольные вопросы …………………………………………….. Задачи для самостоятельной работы …………………………… Библиографический список ……………………………………..

105 106 106 107 108 108 109 110 110 110 111 112 112 113 115 115 116 117 118 119 120 120 121 121 123 123 124 125 127 129 130 132 132 134

Введение Язык программирования Python был задуман как потомок языка программирования ABC. В настоящее время Python – это активно развивающийся высокоуровневый многоцелевой язык программирования. Он поддерживает несколько, наиболее популярных сейчас парадигм программирования, таких как структурное, объектноориентированное, функциональное программирование и другие. Популярности языка способствует то, что он соответствует стандартам Американского национального института стандартов и Международной организации по стандартизации. Python – это многоцелевой язык. Его можно одинаково хорошо использовать для разработки любых программ и их тестирования. Так, например, компания Google широко использует язык Python для своей поисковой системы. Большая часть популярного видеохостинга YouTube была написана на языке Python. Также язык Python применяется в анимационной графике, научных вычислениях и тестировании аппаратного обеспечения. Таким образом, знание Python, одного из популярнейших языков программирования, открывает путь в ведущие IT-компании мира: Google, Яндекс, Mail.Ru, YouTube, Instagram. Python пригоден для решения разнообразных задач и предлагает те же возможности, что и другие языки программирования. Любой язык, неважно – для программирования или общения, состоит, как минимум, из двух частей: словаря и синтаксиса. Язык Python организован аналогично, предоставляя синтаксис для формирования выражений, образующих исполняемые программы, и словарь – набор функциональности в виде стандартной библиотеки и подключаемых модулей. Но благодаря своей лаконичности он позволяет быстрее овладеть синтаксисом языка, чем другие языки программирования. Python является языком общего назначения, поэтому может применяться практически в любой области разработки программного 6

обеспечения (standalone, клиент-сервер, Web-приложения) и в любой предметной области. Кроме того, Python легко интегрируется с уже существующими компонентами, что позволяет внедрять Python в уже написанные приложения. Другая составляющая успеха Python – это его модули расширения, как стандартные, так и специфические. Стандартные модули расширения Python – это отлично спроектированная и неоднократно проверенная функциональность для решения задач, возникающих в каждом проекте по разработке программного обеспечения, обработка строк и текстов, взаимодействие с операционной системой, поддержка Web-приложений. Эти модули также написаны на языке Python, поэтому обладают его важнейшим свойством – кроссплатформенностью, позволяющей безболезненно и быстро переносить проекты с одной операционной системы на другую. Возможности языка программирования Python: — Работа с xml/html файлами. — Работа с http запросами. — GUI (графический интерфейс). — Создание веб-сценариев. — Работа с FTP. — Работа с изображениями, аудио и видео файлами. — Робототехника. — Программирование математических и научных вычислений и многое другое. Синтаксис языка Python проще Pascal. Большинство технических нюансов при программировании в Python решается с помощью встроенных функций. В Python проще и быстрее написать программу начинающим программистам, если она состоит из одной строки, а не из нескольких. В результате алгоритм решает ту же самую задачу, а времени на написание и отладку тратится меньше. Следовательно, можно решить больше заданий и получить больший опыт в написании программ. 7

Последние несколько лет контрольно-измерительные материалы ЕГЭ включают возможность выполнения заданий, в том числе и с использованием языка программирования Python, что актуализирует его изучение как школьниками, так и будущими учителями информатики и ИКТ. Данное учебно-методическое пособие разработано с целью организации начального курса изучения языка программирования Python.

Предисловие Python – высокоуровневый язык программирования общего назначения, ориентированный на повышение производительности разработчика и читаемости кода. Синтаксис ядра Python минималистичен. В то же время стандартная библиотека включает большой объём полезных функций. Python поддерживает структурное, объектно-ориентированное, функциональное, императивное и аспектно-ориентированное программирование. Основные архитектурные черты – динамическая типизация, автоматическое управление памятью, полная интроспекция, механизм обработки исключений, поддержка многопоточных вычислений, высокоуровневые структуры данных. Поддерживается разбиение программ на модули, которые, в свою очередь, могут объединяться в пакеты. Эталонной реализацией Python является интерпретатор CPython, поддерживающий большинство активно используемых платформ. Он распространяется под свободной лицензией Python Software Foundation License, позволяющей применять его без ограничений в любых приложениях, включая проприетарные. Есть реализация интерпретатора для JVM с возможностью компиляции, CLR, LLVM, другие независимые реализации. Проект PyPy использует JITкомпиляцию, которая значительно увеличивает скорость выполнения Python-программ. Разработка языка Python была начата в конце 1980-х годов сотрудником голландского института CWI Гвидо ван Россумом. Для распределённой ОС Amoeba требовался расширяемый скриптовый язык, и Гвидо начал писать Python на досуге, позаимствовав некоторые наработки для языка ABC (Гвидо участвовал в разработке этого языка, ориентированного на обучение программированию). В феврале 1991 года Гвидо опубликовал исходный текст в группе новостей 9

alt.sources. С самого начала Python проектировался как объектноориентированный язык. Название языка произошло вовсе не от вида пресмыкающихся. Автор назвал язык в честь популярного британского комедийного телешоу 1970-х «Летающий цирк Монти Пайтона». Впрочем, всё равно название языка чаще связывают именно со змеёй, нежели с передачей: пиктограммы файлов в KDE или в Microsoft Windows и даже эмблема на сайте python.org (до выхода версии 2.5) изображают змеиные головы. Важная цель разработчиков Python – создавать его забавным для использования. Это отражено в его названии, которое пришло из Монти Пайтона. Также это отражено в иногда игривом подходе к обучающим программам и справочным материалам, таким как примеры использования. Наличие дружелюбного, отзывчивого сообщества пользователей считается наряду с дизайнерской интуицией Гвидо одним из факторов успеха Python. Развитие языка происходит согласно чётко регламентированному процессу создания, обсуждения, отбора и реализации документов PEP (англ. Python Enhancement Proposal) – предложений по развитию Python. 3 декабря 2008 года, после длительного тестирования, вышла первая версия Python 3000 (или Python 3.0, также используется сокращение Py3k). В Python 3000 устранены многие недостатки архитектуры с максимально возможным (но не полным) сохранением совместимости со старыми версиями Python. На сегодня поддерживаются обе ветви развития (Python 3.x и 2.x). Python портирован и работает почти на всех известных платформах – от КПК до мейнфреймов. Существуют порты под Microsoft Windows, практически все варианты UNIX (включая FreeBSD и Linux), Plan 9, Mac OS и Mac OS X, iPhone OS 2.0 и выше, Palm OS, OS/2, Amiga, HaikuOS, AS/400 и даже OS/390, Windows Mobile, Symbian и Android. 10

По мере устаревания платформы её поддержка в основной ветви языка прекращается. Например, с серии 2.6 прекращена поддержка Windows 95, Windows 98 и Windows ME. Однако на этих платформах можно использовать предыдущие версии Python – на данный момент сообщество активно поддерживает версии Python начиная от 2.3 (для них выходят исправления). При этом, в отличие от многих портируемых систем, для всех основных платформ Python имеет поддержку характерных для данной платформы технологий (например, Microsoft COM/DCOM). Более того, существует специальная версия Python для виртуальной машины Java – Jython, что позволяет интерпретатору выполняться на любой системе, поддерживающей Java, при этом классы Java могут непосредственно использоваться из Python и даже быть написанными на Python. Также несколько проектов обеспечивают интеграцию с платформой Microsoft.NET, основные из которых – IronPython и Python.Net. Для написания программы на языке программирования Python необходимо иметь установленный одноименный пакет на персональном компьютере. При его наличии необходимо запустить программу с названием «IDLE». Таким образом запустится графический интерфейс среды разработки языка Python.

Рис. 1. Графический интерфейс среды разработки языка Python 11

Окно среды разработки представляет собой поле ввода текста, которое занимает большую часть площади окна, а также меню. В поле ввода текста изначально записано несколько строк, в которых указана текущая версия языка, дата её публикации, а также приглашение на ввод. Выражения, записанные в этом интерфейсе, вычисляется сразу, потому что Python относится к интерпретируемым языкам программирования. То есть, записываемые на нём команды при каждом выполнении построчно переводятся в двоичный код и выполняются сразу после перевода, а среда разработки языка Python по умолчанию работает в интерактивном режиме. В ней все команды запускаются на выполнение сразу после ввода. Для того, чтобы получить возможность сохранять написанные команды, в среде разработки необходимо создать новый файл. Для этого, в меню File выбирается команда New File или используется сочетание клавиш Ctrl + N при установленной англоязычной раскладке клавиатуры. После этого, поверх окна среды разработки появится окно нового файла. Файлы модулей, описанных на языке Python, сохраняются с расширением *.py, сокращённо от названия языка. При написании кода программы, файл, как правило, сохраняется. Для этого в меню File выбирается команда Save As или используется сочетание клавиш Ctrl + S. После сохранения можно запустить написанную программу на выполнение. Для этого в меню Run выбирается команда Run Module или используется клавиша F5. В главном окне среды разработки будет выводиться результат выполнения программы.

Рис. 2. Стандартный текстовый редактор для написания кода в среде Python 12

Рис. 3. Выполнение написанной программы через команду Run Module

1 ТИПЫ ДАННЫХ. ПРЕОБРАЗОВАНИЕ ТИПОВ ДАННЫХ В Python, как и в других языках программирования, все данные для удобства делятся на типы. Тип данных определяет значения, которые можно присваивать, и действия, которые можно выполнять. 1.1 Основы работы с типами данных Примером разбиения на типы данных служат разные числовые множества, изучаемые в курсе математики: целые числа (0, ±1, ±2…), иррациональные числа (π) и т. п. Как правило, в математических операциях можно комбинировать числа различных типов, например: 5 + ����.

При этом можно оставить в качестве ответа полученное уравнение, а также можно округлить π до 3,14 и сложить числа: 5 + ���� = 5 + 3,14 = 8,14.

Но если попытаться решить уравнение, в котором кроме чисел будут присутствовать другие типы данных, например слова, не получится никакого вменяемого результата. К примеру, имя + 7.

Компьютеры строго разграничивают все типы данных, потому нужно очень внимательно присваивать значения и использовать их в различных операциях.

Powered by TCPDF (www.tcpdf.org)

1.2 Числа Любая цифра воспринимается в Python как число. При этом не обязательно объявлять, какой тип данных вы вводите. Python воспринимает любое число, записанное без десятичных знаков, как целое число (например, 133), а любое число, записанное с десятичными знаками, в качестве числа с плавающей точкой (например, 138.0). Целые числа Как и в математике, в компьютерном программировании целые числа – это натуральные числа, им противоположные (отрицательные) и 0 (…, -1, 0, 1, …). Целое число можно отметить как int. Как и в других языках, в Python не нужно использовать запятую при написании многозначных чисел (к примеру, тысяча записывается как 1000, а не как 1,000). Синтаксис вывода целого числа: print(- 25)

Результат: -25. Можно объявить переменную (в данном случае она является символом числа, которое нужно вывести): my_int = -25 print(my_int)

Результат: -25 Целые числа в Python 3 неограниченного размера. Числа с плавающей точкой Число с плавающей точкой (или float) – это действительное число (то есть оно может быть как рациональным, так и иррациональным). Числа с плавающей точкой могут содержать дробную часть (например, 9.0 или -116.42). То есть Python воспринимает любое число с десятичной точкой как число с плавающей точкой. Синтаксис вывода числа с плавающей точкой: print(17.3)

Результат: 17.3 Можно объявить переменную: 15

my_flt = 17.3 print(my_flt)

Результат: 17.3 При работе с целыми числами и числами с плавающей точкой важно помнить, что 3 и 3.0 – не одно и то же. 3 ≠ 3.0,

поскольку 3 – целое число, а 3.0 – число с плавающей точкой. Логический тип Логические значения в Python представлены двумя величинами – логическими константами True (Истина) и False (Ложь). Логические значения получаются в результате логических операций и вычисления логических выражений. Логический тип данных (или Boolean) – это тип данных, который принимает одно из двух возможных значений: True или False. Этот тип присутствует во многих языках программирования и используется для построения алгоритмов. Примечание. Название этого типа данных (Boolean) всегда пишется с заглавной буквы, поскольку этот тип назван в честь математика Джорджа Буля, который занимался вопросами математической логики. Значения True и False также пишутся с большой буквы. Многие математические операции можно расценивать как истинные или ложные: 500 > 100 (True); 1 > 5 (False).

Как и в случае с числами, значения Boolean можно определять переменными: my_bool = 5 > 8

Теперь можно вывести значение переменной с помощью функции print(): print (my_bool)

Поскольку 5 меньше 8, на экране появится:

1.3 Строки Строка представляет собой последовательность из одного или нескольких символов (букв, цифр и других символов), которые могут быть постоянными или переменными. В Python cтроки обозначаются одинарными (‘) или двойными кавычками (»). Для создания строки последовательность символов заключается в кавычки: ‘This is a string in single quotes.’ »This is a string in single quotes.»

Одинарные и двойные кавычки работают одинаково. Важно только использовать один и тот же тип кавычек в рамках одной программы. Простая программа «Hello, World» демонстрирует применение строк в программировании (последовательность символов, из которых состоит фраза »Hello, World!», является строкой). print (»Hello, World!»)

Строки можно хранить в переменных:

hm = »Hello, World!»

Чтобы вывести значение переменной, вводится:

Результат: Hello, World! Списки Список – это изменяемая, упорядоченная последовательность элементов. Значения, которые находятся в списке, называются элементами. Подобно тому, как строки определяются кавычками, списки определяются квадратными скобками [ ]. Список целых чисел выглядит так: [-3, -2, -1, 0, 1, 2, 3]

Список чисел с плавающей точкой имеет такой вид:

[3.14, 9.23, 111.11, 312.12, 1.05] 17

[‘shark’, ‘cuttlefish’, ‘squid’, ‘mantis shrimp’]

Определим список sea_creatures и осуществим его вывод:

sea_creatures = [‘shark’, ‘cuttlefish’, ‘squid’, ‘mantis shrimp’] print (sea_creatures) [‘chark’, ‘cuttlefish’, ‘squid’, ‘mantis shrimp’]

Списки – очень гибкий тип данных, который позволяет быстро добавить, удалить или изменить данные. В Python существует тип данных, очень похожий на списки, но который нельзя изменять. Такой тип называется кортежем. Кортежи Кортеж (tuple) позволяет группировать данные. Кортеж – это неизменяемая упорядоченная последовательность элементов. Кортежи очень похожи на списки, но вместо квадратных скобок они используют круглые. Данные этого типа нельзя изменять. Кортеж имеет такой вид: (‘blue coral’, ‘staghorn coral’, ‘pillar coral’)

Кортеж можно хранить в переменной и вывести на экран:

coral = (‘blue coral’, ‘staghorn coral’, ‘pillar coral’) print (coral)

Результат: (‘blue coral’, ‘staghorn coral’, ‘pillar coral’) Словари Словарь – это неупорядоченный изменяемый массив данных, состоящий из пар «ключ – значение». Словари обозначаются фигурными скобками < >. Словари обычно хранят связанные данные. Словарь имеет такой вид:

Кроме фигурных скобок, в словарях используется двоеточие. Слева от двоеточия пишутся ключи, в данном случае это ‘name’, ‘animal’, ‘color’, ‘location’. 18

Справа от двоеточия находятся значения. Значения могут быть представлены любым типом данных. В приведённом примере значениями являются ‘Jake’, ‘dog’, ‘yellow’, ‘Tree Fort’. Создать переменную для словаря, а затем вывести её на экран. jake = print(jake)

Результат: . Чтобы запросить только один из элементов словаря, используются квадратные скобки. Например: print (jake [‘color’])

2 ПРЕОБРАЗОВАНИЕ ТИПОВ ДАННЫХ В PYTHON В программировании часто необходимо конвертировать один тип данных в другой, чтобы получить доступ к другим функциям, например, склеить числовые значения со строками или представить целые числа в виде десятичных. 2.1 Преобразование числовых типов В Python существует два числовых типа данных: целые числа и числа с плавающей точкой. Для преобразования целых чисел в числа с плавающей точкой и наоборот Python предоставляет специальные встроенные методы. Преобразование целых чисел в числа с плавающей точкой Метод float() преобразует целые числа в числа с плавающей точкой. Число указывается в круглых скобках: float(57)

В результате преобразует число 57 в 57.0. Также можно использовать переменные. Например:

f = 57 print (float(f))

Результат: 57.0 Преобразование чисел с плавающей точкой в целые числа Встроенная функция int() предназначена для преобразования чисел с плавающей точкой в целые числа. Например, число 390.8 преобразуется в 390: int (390.8)

Эта функция также может работать с переменными:

b = 125.0 c = 390.8 print (int (b)) print (int (c))

Результат: 125 390. Чтобы получить целое число, функция int() отбрасывает знаки после запятой, не округляя их (потому 390.8 не преобразовывается в 391). Преобразование чисел с помощью деления При делении Python 3 может преобразовать целое число в число с плавающей точкой (в Python 2 такой функции нет). К примеру, разделив 5 на 2, получается 2.5. a = 5 / 2

Результат: 2.5. Python не преобразует тип данных во время деления; следовательно, разделив целое число на целое число, в результате получили бы целое число 2. 2.2 Преобразование строк Существует множество способов преобразования строк. Преобразование чисел в строки Чтобы конвертировать число в строку, используется метод str(). Число (или переменная) помещается в круглые скобки. Можно преобразовать целое число, например: str (12)

’12’ Кавычки означают, что теперь 12 является строкой, а не числом. Особенно полезно преобразовывать числа в строки, используя переменные. К примеру, можно отследить, сколько строк кода в день пишет тот или иной пользователь. Если пользователь пишет больше 50 строк, программа отправит ему поощрительное сообщение. user = »Michael» lines = 50

print (»Congratulations, » + user + »! You just wrote » + lines + » lines of code. »)

Запустив этот код, получится ошибка:

TypeError: Can’t convert ‘int’ object to str implicitly

Python не может склеивать строки с числами, потому нужно преобразовать значение lines в строку.

user = »Michael» lines = 50 print (»Congratulations, » + user + »! You just wrote » + str(lines) + » lines of code.»)

Теперь, запустив код:

Метод str() может преобразовать в строку и число с плавающей точкой. Необходимо поместить в круглые скобки число или переменную: f = 5524.53 print (str (421.034)) print (str (f))

Результат: ‘421.034’ ‘5524.53’ Выполняется конкатенация (склеивание) строки и преобразованного в строку числа: f = 5524.53 print (»Michael has » + str(f) + »points.»)

Результат: Michael has 5524.53 points. Преобразование строк в числа Строки можно преобразовать в числа с помощью методов int() и float(). Если в строке нет десятичных знаков, лучше преобразовать её в целое число. Для этого используется int().

Можно попробовать расширить предыдущий пример кода, который отслеживает количество написанных строк. Пусть программа отслеживает разницу между написанными строками вчера и сегодня. lines_yesterday = »50» lines_today = »50» lines_more = lines_today – lines_yesterday print (lines_more)

Результат: TypeError: unsupported operand type (s) for -: ‘str’ and

‘str’. При запуске возникла ошибка, поскольку Python не может выполнить сложение строк. Для этого необходимо преобразовать строки в числа и снова запустить программу: lines_yesterday = »50» lines_today = »108» lines_more = int (lines_today) – int (lines_yesterday) print (lines_more)

Результат: 58. Значение переменной lines_more – это число, в данном случае это 58. Также можно преобразовать числа в предыдущем примере в числа с плавающей точкой. Для этого используется метод float(). К примеру, очки начисляются в десятичных значениях. total_points = »5524.53» new_points = »45.30» new_total_points = total_points + new_points print (new_total_points)

Результат: 5524.5345.30. В данном случае оператор + склеивает две строки, а не складывает числа. Потому в результате получилось довольно странное значение. Следует конвертировать эти строки в числа с плавающей точкой, а затем выполнить сложение. total_points = »5524.53» new_points = »45.30»

new_total_points = float (new_points) print (new_total_points)

Результат: 5569.83. Теперь программа возвращает ожидаемый результат. Если попробовать преобразовать строку с десятичными значениями в целое число, получим ошибку: f = »54.23» print (int(f))

Результат: ValueError: invalid literal for int () with base 10: ‘54.23’. Преобразование в кортежи и списки Чтобы преобразовать данные в кортеж или список, следует использовать методы tuple() и list() соответственно. Преобразование списка в кортеж Преобразовывая список в кортеж, можно оптимизировать программу. Для преобразования в кортеж используется метод tuple(). print (tuple ([‘pull request’, ‘open source’, ‘repository’, ‘branch’]))

Результат: (‘pull request’, ‘open source’, ‘repository’, ‘branch’). Выведенные на экран данные являются кортежем, а не списком, поскольку они взяты в круглые скобки. Возможно использование tuple() с переменной: sea_creatures = [‘shark’, ‘cuttlefish’, ‘squid’, ‘mantis shrimp’] print (tuple (sea_creatures))

Результат: (‘shark’, ‘cuttlefish’, ‘squid’, ‘mantis shrimp’). В кортеж можно преобразовать любой итерируемый тип, включая строки: print (tuple (‘Michael’))

Результат: (‘M’, ‘i’, ‘c’, ‘h’, ‘a’, ‘e’, ‘l’). Конвертируя в кортеж числовой тип данных, получится ошибка:

Результат: TypeError: ‘int’ object is not iterable. 24

Преобразование в списки Можно преобразовать кортеж в список, чтобы сделать его изменяемым. Следует обратить внимание: при этом в методах list() и print() используется две пары круглых скобок. Одни принадлежат собственно методу, а другие – кортежу. print (list coral’)))

Результат: [‘blue coral’, ‘staghorn coral’, ‘pillar coral’]. Если данные, которые вывел метод print, заключены в квадратные скобки, значит, кортеж преобразовался в список. Чтобы избежать путаницы с круглыми скобками, можно создать переменную: colar = (‘blue coral’, ‘staghorn coral’, ‘pillar coral’) list (colar)

Строки тоже можно преобразовывать в списки:

print (list (‘Michael’))

Результат: [‘M’, ‘i’, ‘c’, ‘h’, ‘a’, ‘e’, ‘l’]. Контрольные вопросы

1. Назовите типы данных в языке Python. 2. Что означает термин «неизменяемый» и какие типы данных языка Python являются неизменяемыми? 3. Что означает термин «последовательность» и какие типы относятся к этой категории? 4. Для преобразования каких чисел предназначена встроенная функция int()? 5. Изменяемая упорядоченная последовательность элементов, взятая в квадратные скобки ([ ]) в Python – это? 6. В каком случае используется метод str()? 7. С помощью каких методов можно преобразовать строки в числа? 25

8. Какой метод используется для преобразования списка в кортеж? Задачи для самостоятельного решения 1. Дано четырехзначное число. Поменяйте местами наименьшую и наибольшую цифры. 2. Найдите n пар простых чисел, которые отличаются друг от друга на 2. 3. Вывести все пятизначные числа, которые делятся на 2, у которых средняя цифра нечетная и сумма всех цифр делится на 4. 4. Дана строка, состоящая ровно из двух слов, разделенных пробелом. Переставьте эти слова местами. Результат запишите в строку и выведите получившуюся строку. При решении этой задачи нельзя пользоваться циклами и инструкцией if. 5. Дана строка. Если в этом числе буква f встречается только один раз, выведите её индекс. Если она встречается два и более раз, выведите индекс её первого и последнего появления. Если буква f в данной строке не встречается, ничего не выводите. При решении этой задачи нельзя использовать метод count и циклы. 6. Дана строка. Найдите в этой строке второе вхождение буквы f и выведите индекс этого вхождения. Если буква f в данной строке встречается только один раз, выведите число -1, а если не встречается ни разу, выведите число -2. При решении этой задачи нельзя использовать метод count. 7. Дана строка. Замените в этой строке все цифры 1 на слово one. 8. Дана строка. Удалите из этой строки все символы @. 9. Дана строка. Получите новую строку, вставив между двумя символами исходной строки символ *. Выведите полученную строку. 10. Дана строка. Удалите из нее все символы, индексы которых делятся на 3. 26

3 ВВОД И ВЫВОД ДАННЫХ Компьютерные программы нужны для того, чтобы быстро обрабатывать и анализировать некоторые данные, особенно это целесообразно, когда речь идет о больших объемах данных. Предположим, что есть программа, способная обработать некоторые данные. Прежде чем обработать данные, программа должна их каким-то образом получить. Так, например, пользователь может задать данные путём ввода с клавиатуры. После того, как программа обработала данные, она должна определённым способом вернуть результат их обработки пользователю, например, вывести его на экран в текстовой форме. Именно для того, чтобы организовать передачу данных от пользователя программе, и наоборот, используются инструкции ввода и вывода. Начнём с ввода данных. Для того, чтобы дать возможность пользователю ввести данные, используется функция input без параметров. Эта функция возвращает значение, которое пользователь ввёл с клавиатуры в строку. Рассмотрим синтаксис. Все функции в языке Python записываются в составе инструкций. Для вызова функции записывается её имя, после которого в скобках следуют её параметры. Так как функция input не имеет параметров, после её имени должны следовать пустые скобки. Так как программа записывает данные в переменную, то результат работы этой функции присваивается некоторой переменной. Таким образом, для считывания значения переменной a с клавиатуры нужно записать инструкцию присваивания переменной a значения функции input (). Для вывода данных из оперативной памяти компьютера на экран монитора используется инструкция print. После служебного слова print в скобках следует список выводимых данных. Как и в любой другой операции, в инструкции вывода могут указываться литералы, переменные и выражения. 27

Рассмотрим пример. На ввод подать 2 целых числа и вывести на экран их сумму.

a = input () b = input () print (a + b)

Первое число зададим равным 35. Второе число зададим равным 42. Очевидно, что, по нашему замыслу, программа должна была вывести на экран число 77, но вместо этого она вывела 3542. Хотя на самом деле это не названное число, а символьная строка, состоящая из четырёх цифр. Почему так произошло? Пользователь, задавая данные с клавиатуры, вводит их в текстовой форме. То есть функция input возвращает данные типа str, по условию задачи, нужны целые числа, то есть данные типа int. Для того, чтобы эти данные получить, необходимо воспользоваться функцией преобразования типов. a = int (input ()) b = int (input ()) print (a + b)

В языке программирования Python есть множество функций и одна из них print() – это команда языка Python, которая выводит то, что находится в скобках, на экран: print(1032)

Результат: Hello В скобках могут быть любые типы данных. Кроме того, количество данных может быть различным: print(«a:», 1)

Powered by TCPDF (www.tcpdf.org)

three = 3 print(one, two, three)

Результат: 1 2 3. Можно передавать в функцию print() как непосредственно литералы (в данном случае «a:» и 1), так и переменные, вместо которых будут выведены их значения. Аргументы функции (то, что в скобках) разделяются между собой запятыми. В выводе вместо запятых значения разделены пробелом. Если в скобках стоит выражение, то сначала оно выполняется, после чего print() уже выводит результат данного выражения: print(«hello» + » » + «world»)

Результат: hello world;

Результат: 8.75. В print() предусмотрены дополнительные параметры. Например, через параметр sep можно указать отличный от пробела разделитель строк: print(«Mon», «Tue», «Wed», «Thu», «Fri», «Sat», «Sun», sep=»-«)

Результат: 1//2//3 Параметр end позволяет указывать, что делать, после вывода строки. По умолчанию происходит переход на новую строку. Однако это действие можно отменить, указав любой другой символ или строку: print(10, end=»»)

Результат: 10. Обычно, если end используется, то не в интерактивном режиме, а в скриптах, когда несколько выводов подряд надо разделить не переходом на новую строку, а, скажем, запятыми. Сам переход на новую строку обозначается комбинацией символов «\n». Если присвоить это значение параметру end, то никаких изменений в работе 29

функции print() вы не увидите, так как это значение и так присвоено по умолчанию: print(10, end=’\n’)

Результат: 10. Однако, если надо отступить на одну дополнительную строку после вывода, то можно сделать так: print(10, end=’\n\n’)

Результат: 10. Рассмотрим еще одну возможность функции print() – это использование форматирования строк. На самом деле это никакого отношения к print() не имеет, а применяется к строкам. Но обычно используется именно в сочетании с функцией print(). Форматирование может выполняться в так называемом старом стиле или с помощью строкового метода format. Старый стиль также называют Си-стилем, так как он схож с тем, как происходит вывод на экран в языке C. Рассмотрим пример: pupil = «Ben» old = 16 grade = 9.2 print(«It’s %s, %d. Level: %f» % (pupil, old, grade))

Результат: It’s Ben, 16. Level: 9.200000. Здесь вместо трех комбинаций символов %s, %d, %f подставляются значения переменных pupil, old, grade. Буквы s, d, f обозначают типы данных – строку, целое число, вещественное число. Если бы требовалось подставить три строки, то во всех случаях использовалось бы сочетание %s. Хотя в качестве значения переменной grade было указано число 9.2, на экран оно вывелось с дополнительными нулями. Однако можно указать, сколько требуется знаков после запятой, записав перед буквой f точку с желаемым числом знаков в дробной части: print(«It’s %s, %d. Level: %.1f» % (pupil, old, grade))

Результат: It’s Ben, 16. Level: 9.2. Теперь рассмотрим метод format(): 30

print(«This is a . It’s .».format(«ball», «red»))

Результат: This is a ball. It’s red.

print(«This is a . It’s .».format(«cat», «white»))

Результат: This is a cat. It’s white.

print(«This is a . It’s .».format(1, «a», «number»))

Результат: This is a 1. It’s a number. В строке в фигурных скобках указаны номера данных, которые будут сюда подставлены. Далее к строке применяется метод format(). В его скобках указываются сами данные (можно использовать переменные). На нулевое место подставится первый аргумент метода format(), на место с номером 1 – второй и т. д. При обработке вывода данных часто бывает полезным использование форматированного вывода. В этом случае можно выделить некоторое количество знаковых позиций для вывода каждого значения. Для этого используется функция «Формат», которая формирует символьную строку заданного формата. Для этого присвоим переменным a, b и c соответственно значения 15, 141 и 3. Дальше записываем инструкцию print, в которой, в кавычках, сначала запишем строку, описывающую формат вывода. Формат вывода каждого отдельного значения указывается в отдельных фигурных скобках. Он начинается с двоеточия. Дальше для целых чисел следует единственное число – количество выделяемых знаковых позиций и английская буква d. Выделим по пять знаковых позиций для вывода каждого числа. После строки формата ставится точка и записывается служебное слово format, после которого в скобках указываются выводимые значения. Укажем значения переменных a, b и c. Программа вывела отступы перед значениями, так как незанятые знаковые позиции заполняются пробелами. Если же знаковых позиций не хватает, они дополняются автоматически. a = 15 b = 141 c = 3

Результат: 15 141 3. Рассмотрим расчёт и вывод частного двух чисел, причём необязательно целых. Так как числа необязательно будут целыми, то вводимые значения нужно преобразовать в вещественный тип float.

a = int (input ()) b = int (input ()) print (a, ‘/’, b, ‘=’, a / b, sep = »)

Введём первое число, равное 0.01, а второе – 5000. В результате программа вывела вместо последнего числа сообщение: 2e-06. Это называется экспоненциальной формой представления числа. Она означает, что результат равен произведению 2 и 10-6. Для вывода вещественных значений также можно использовать форматированный вывод. Применим форматированный вывод для последнего числа. В качестве строки формата, в кавычках, между фигурными скобками укажем двоеточие, после которого будет следовать два целых числа, разделённых точкой – общее количество выделяемых знаковых позиций и количество выводимых знаков после запятой. Укажем 10 знаковых позиций и 7 знаков после запятой. Дальше для вещественных чисел следует английская буква f. После кавычек поставим точку и напишем служебное слово format, после которого в скобках укажем выводимое значение. print (‘Программа, вычисляющая частное двух чисел. Введите два числа.’) a = int (input ()) b = int (input ()) print (a, ‘/’, b, ‘=’, ».format (a / b))

Запустим модуль на выполнение. Снова зададим числа 0.01 и 5000. На этот раз программа вывела ответ не в экспоненциальной, а в обычной форме.

Контрольные вопросы 1. С помощью какой команды производится ввод данных на языке программирования Python? 2. С помощью какой команды производится вывод данных на языке программирования Python? 3. Можно ли введенные данные сразу преобразовывать в другой тип? 4. В переменную “a” записали числовое значение 42. Что выведет на экран следующая команда: print(a)? 5. В переменные “a” и “b” записали числовые значения 2 и 5 соответственно. Что выведет на экран следующая команда: print(a + b)? 6. В переменные “a” и “b” записали текстовые значения “2” и “5” соответственно. Что выведет на экран следующая команда: print(a + b)? 7. Как при вводе данных преобразовывать текст в числовые значения? 8. Будет ли работать программа, если внутри команды print объявить команду input? 9. Можно ли в команде print перечислять данных для вывода? 10. Правда ли, что строки можно складывать? Задачи для самостоятельного решения 1. Напишите программу, которая запрашивает имя пользователя и затем выводит приветствие с введенным именем. 2. Напишите программу, в которой запрашивается несколько раз ввод данных, а затем эти данные суммируются, выводясь на экран. 3. Напишите программу, которая запрашивает 2 ввода чисел и выводит их сумму. 33

4. Напишите программу, которая вычисляет дискриминант квадратного уравнения (значения вводятся пользователем). 5. Напишите программу, которая вычисляет периметр треугольника (значения вводятся пользователем). 6. Напишите программу, которая считает квадрат суммы двух введенных чисел (значения вводятся пользователем). 7. Напишите программу, которая возводит в куб любое введенное число (значение вводится пользователем). 8. Напишите программу, которая вычисляет значение выражения: 4 ∙ 100 – 54. 9. Напишите программу, которая запрашивает имя пользователя и затем выводится прощание с введенным именем. 10. Напишите программу, которая вычисляет площадь прямоугольника (значения вводятся пользователем).

4 АРИФМЕТИЧЕСКИЕ ОПЕРАЦИИ Числа в Python делятся на целые; действительные (с плавающей точкой размером в 64 бита); комплексные (с плавающей точкой размером в 128 бит). Если в качестве операндов (операнды – это действия операторов, производимые над данными) арифметического выражения используются только целые числа, то результат будет целое число. Исключение: операция деления, результатом которой является вещественное число. При совместном использовании целочисленных и вещественных переменных результат будет вещественным. Оператор – это символ или функция, которая выполняет то или иное действие над данными. Таблица 1 Математические операторы Оператор

сумма x и y (6 + 3 = 9)

разность x и y (57- 2 = 55) произведение x и y (3 * 3 = 9) x на y: 4 / 1.6 = 2.5, 10 / 5 = 2.0, результат всегда типа float x на y нацело: 11 // 4 = 2, 11.8 // 4 = 2.0, результат целый, только если оба аргумента целые x % y возведение x в степень y: (2 ** 3 = 8) округление (12,3) =12

x ** y round(x) round(x,n) pow(x, y) divmod(x, y)

округляет число x до n знаков после запятой полный аналог записи x ** y выдаёт два числа: частное и остаток, обращаться следует так q, r = divmod(x, y) Меньше. Определяет, верно ли, что x меньше y. Все операторы сравнения возвращают True или False Больше. Определяет, верно ли, что x больше y 35

Окончание таблицы 1 1

Операции сложения, вычитания, умножения и возведения в степень выдают ответ типа int только тогда, когда оба аргумента целые. Если один из аргументов действительный, а другой целый, то выдают ответ типа float. Если хотя бы один аргумент комплексный, то выводит ответ типа complex. Операция возведения в степень выдает комплексный результат при возведении отрицательных чисел в степень кроме целой и нечетной степени. 4.1 Сложение Складывать можно сами числа:

Результат: 6. Либо можно складывать переменные, но они должны заранее быть проинициализированы:

a = 4 b = 2 print(a + b)

Результат: 6. Результат операции сложения может быть присвоен другой переменной: 36

a = 2 b = 4 c = a + b print(c)

Результат: 6 Либо может быть присвоен ей самой, тогда можно использовать полную или сокращенную запись. Полная выглядит так:

a = 2 b = 4 a = a + b print(a)

Результат: 6. Сокращенная запись:

a = 2 b = 4 a += b print(a)

Результат: 6. Числа с плавающей точкой:

e = 5.5 f = 2.5 print(e + f)

Результат: 8.0. В результате сложения чисел с плавающей точкой получается число с плавающей точкой, потому Python выводит 8.0, а не 8. Таблица 2 Арифметические действия Арифметическое действие 1 Вычитание

Сокращенная запись 2 print (6-2) 4

Полная запись 3

a = 5 b = 7 print(a – b) -2

Окончание таблицы 2 1 Умножение

Получение целой части от деления Получение остатка от деления

Возведение в степень

print (9 // 3) 3 print (9 % 5) 4 print (5 ** 4) 625

a = 5 a *= 10 print(a) 50 a = 7 b = 4 print (a 1.75 a = 7 b = 4 print (a 1 a = 7 b = 4 print (a 3 a = 10 b = 10 print (a 100

Операторы присваивания В Python есть составные операторы присваивания для каждой математической операции. Операторы присваивания позволяют постепенно увеличить или уменьшить значение, а также автоматизировать некоторые вычисления. Таблица 3 Операторы присваивания Оператор Действие 1 2 y += x Сложение и присваивание. Составной оператор += выполнил сложение, а затем присвоил переменной y значение, полученное в результате сложения

Пример 3 с = 5 а = 2 с += а 7

Окончание таблицы 3 1 y-=x

2 3 с = 5 Вычитание и присваивание. Отнимает значение правого операнда от лево- а = 2 с — = а го и присваивает результат левому операнду 3 с = 5 Умножение и присваивание. Умножает правый операнд на левый операнд и а = 2 с *= а присваивает результат левого операнда 10 с = 10 Деление и присваивание. Делит левый операнд на правый операнд и а = 2 с /= а присваивает результат левого операнда 5 с = 11 Деление floor и присваивание. Выполняет деление операторов с округлением а = 2 с //= а и присваивает значение левого операнда 5 с = 3 возведение в степень и присваивание. а = 2 Выполняет вычисление экспоненту от операторов и присваивает значение левого операнда с **= а 9 с = 5 Вывод остатка и присваивание. Принимает модуль с помощью двух операндов а = 2 с %= а и присваивает результат левого операнда 1

4.2 Унарные арифметические операции Унарное математическое выражение состоит только из одного компонента или элемента. В Python «плюс» и «минус» вместе со значением могут быть использованы в качестве одного элемента, это позволяет показать тождественность значения (+) или изменить его знак (-). Тождественность используется нечасто. Плюс можно использовать с положительными числами: i = 3.3 print(+i)

Результат: 3.3. Если используется плюс с отрицательным числом, он также вернёт тождественное (в этом случае – отрицательное) число. j = -19 print(+j)

Результат: -19. Минус позволяет изменить знак. Если вы добавите минус к положительному значению, в результате будет отображено отрицательное значение: i = 3.3 print(-i)

Результат: -3.3. Если добавить минус к отрицательному значению, в результате получится положительное число: j = -19 print(-j)

Результат: 19. 4.3 Работа с комплексными числами

При создании комплексного числа используют функцию complex(a, b), у которой первый аргумент передает действительную часть, а второй – мнимую часть. Либо может записать число в виде ���� + ��������. Создание комплексного числа:

z = 1 + 2j print(z)

x = complex(3, 2) print(x)

Таблица 4 Комплексные числа Арифметическое действие Сложение

Запись в Python print(x + z) (4+4j) print(x – z) Вычитание (2+0j) print(x * z) Умножение (-1+8j) print(x / z) Деление (1.4-0.8j) print(x ** z) Возведение в степень (-1.11227220363633930.012635185355335208j) print(x ** 3) (-9+46j) Извлечение действительной и x = 3 + 2j print(x.real) мнимой части 3.0 print(x.imag) 2.0 Получение комплексно со- x = 3 + 2j print (x.conjugate()) пряженного числа (3-2j)

4.4 Битовые операции Над числами int в Python можно выполнять и битовые операции. Побитовые операторы предназначены для работы с данными в битовом (двоичном) формате. Предположим, что у нас есть два числа a = 60 и b = 13. В двоичном формате они будут иметь следующий вид: ���� = 00111100 ���� = 00001101 41

Таблица 5 Битовые операции Операция

Побитовое «и» Бинарный «И» оператор копирует бит в результат только если бит присутствует в обоих операндах

a&b 12 в двоичном формате 0000 1100

Бинарный «ИЛИ» оператор копирует бит, если тот присутствует в хотя бы в одном операнде

a|b 61 в двоичном формате 0011 1101

Побитовое исключающее «или» Бинарный «ИЛИ» оператор копирует бит, если бит присутствует в одном из операндов, но не в обоих сразу

a^b 49 в двоичном формате 0011 0001

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

a >> 2 15 в двоичном формате 0000 1111

Инверсия битов Является унарным (то есть ему нужен только один операнд) меняет биты на обратные, там, где была единица, становится ноль и наоборот

~a 195 в двоичном формате 1100 0011

4.5 Базовые операции над строками Арифметические операции. Для строк, подобно числам, определены операторы сложения «+» и умножения «∗». 42

Powered by TCPDF (www.tcpdf.org)

В результате сложения содержимое двух строк записывается подряд в новую строку, например: S1 = ‘Py’ S2 = ’thon’ S3 = S1 + S2 print (S3)

Результат: ’Python’. Можно складывать несколько строк подряд. Умножение определено для строки и целого положительного числа, в итоге получается новая строка, которая повторяет исходную столько раз, какое было значение числа (возьмём строку S3 из прошлого примера): print(S3 * 4)

Результат: ’PythonPython’. 4.6 Приоритет операций В Python операции выполняются в порядке их приоритета. Например: ���� = 20 + 20 ∗ 2

Сначала выполняется умножение (20 ∗ 2 = 40), а затем сложение (20 + 40). Потому результат будет такой: print(u)

Результат: 60. Чтобы сначала выполнить операцию сложения, а затем умножить полученный результат на 5, нужно взять сложение в скобки: u = (10 + 10) * 5 print(u)

Таблица 6 Приоритет операторов Оператор =, !=, == | ^ &

Значение оператора Сравнения Побитовое «ИЛИ» Побитовое «ИСКЛЮЧИТЕЛЬНО ИЛИ» Побитовое «И» Сдвиги Сложение и вычитание Умножение, деление, целочисленное деление и остаток от деления Положительное, отрицательное Побитовое НЕ Возведение в степень

Контрольные вопросы 1. Какие арифметические операции реализованы в Python? 2. Чем отличаются унарные операции от бинарных? Приведите примеры. 3. Перечислите типы ответов при выполнении арифметических операций. От чего они зависят? 4. Приведите примеры выполнения сложения и вычитания. 5. Приведите примеры выполнения округления, возведения в степень. 6. Как получить целую часть и остаток от деления? 7. Как получить комплексное число? Что такое приоритет операций? 8. Какие арифметические операции возможны с комплексными числами? 9. Что такое битовые операции? 10. Какие арифметические операции над строками возможны?

Задачи для самостоятельного решения 1. Дано целое число, большее 999. Используя одну операцию деления нацело и одну операцию взятия остатка от деления, найти цифру, соответствующую разряду сотен в записи этого числа. 2. Вводится натуральное число (целое больше нуля). Необходимо найти сумму и произведение цифр, из которых состоит это число. 3. Дана масса M в килограммах. Используя операцию деления нацело, найти количество полных тонн в ней (1 тонна = 1000 кг). 4. Дано двузначное число. Вывести число, полученное при перестановке цифр исходного числа. 5. Напишите программу (необходимые данные вводятся с клавиатуры) для вычисления всех трёх сторон прямоугольного треугольника, если даны один из острых углов и площадь. 6. Составьте арифметическое выражение и вычислите n-е чётное число (первым считается 2, вторым 4 и т. д.). 7. Вы стоите на краю дороги и от вас до ближайшего фонарного столба x метров. Расстояние между столбами y метров. На каком расстоянии от вас находится n-й столб? 8. ���� — вещественное число. Запишите выражение, позволяющее выделить его дробную часть. 9. Старинными русскими денежными единицами являются: 1 рубль = 100 копеек, 1 гривна = 10 копеек, 1 алтын = 3 копейки, 1 полушка = 0,25 копейки. Имеется A копеек. Разложите имеющуюся сумму в копейках на сумму из x рублей + y гривен + z алтынов + v полушек. 10. Даны два предмета, первый из них стоит A рублей и B копеек, второй стоит C рублей D копеек. Сколько рублей и копеек они стоят в сумме? 11. Дано действительное положительное число ���� и целое неотрицательное число n. Вычислите �������� . 12. Для натуральных чисел A и B требуется вычислить значение A в степени B. 45

5 ЛОГИЧЕСКИЕ ОПЕРАЦИИ Логический тип данных (Boolean) служит для хранения значений, которые обладают одним из двух возможных состояний: истина (True) или ложь (False). Логический тип данных нужен, чтобы программировать альтернативные действия. 5.1 Операторы сравнения В программировании операторы сравнения используются при оценке и сравнении значений для последующего сведения их к одному логическому значению (True или False). Операторы сравнения Python 3 представлены в таблице 7. Таблица 7 Операторы сравнения Python 3 Оператор

Проверяет равенство между компонентами; условие истинно, если компоненты равны

Проверяет равенство между компонентами; условие истинно, если компоненты НЕ равны

Оценивает значение левого компонента; условие истинно, если он больше, чем правый

Оценивает значение левого компонента; условие истинно, если он больше или равен правому компоненту 46

Рассмотрим пример работы. Пусть дана пара переменных. Произведём сравнение значений переменных с помощью вышеперечисленных операторов.

x = 5 y = 8 print(«x print(«x print(«x print(«x print(«x print(«x

== y:», x == y) != y:», x != y) y:», x > y) = y)

Результат: x == y: False x != y: True x y: False x = y: False. Python оценивает соотношения между значениями переменных

так: — 5 равно 8? Ложь — 5 не равно 8? Истина — 5 меньше 8? Истина — 5 больше 8? Ложь — 5 меньше или равно 8? Истина — 5 больше или равно 8? Ложь. Также операторы сравнения можно применять к числам с плавающей точкой и строкам. Примечание. Строки чувствительны к регистру; чтобы отключить такое поведение, нужно использовать специальный метод. Сравним две строки:

Hello = «Hello» hello = «hello» print(«Hello == hello: «, Hello == hello) 47

Результат: Hello == hello: False. Строки Hello и hello содержат одинаковый набор символов, однако они не равны, поскольку одна из них содержит символы верхнего регистра. Добавим ещё одну переменную, которая также будет содержать символы верхнего регистра, и сравним.

Hello = «Hello» hello = «hello» Hello_there = «Hello» print(«Hello == hello: «, Hello == hello) print(«Hello == Hello_there», Hello == Hello_there)

Результат: Hello == hello: False Hello == Hello_there: True. Также для сравнения строк можно использовать операторы > и 7) and (2 Оба выражения истинны, потому 1.4 ложно. (False) and (True) = False. Далее оператор not заменит полученное значение False на обратное ему логическое значение: not(False) = True. Значит, результат будет таким: True.

5.3 Таблицы истинности Ниже представлены таблицы истинности для оператора сравнения == и всех логических операторов. Таблица 9 Таблица истинности оператора == x

Python для начинающих

Министерство науки и высшего образования Российской Федерации Орский гуманитарно-технологический институт (филиал) федерального государственного бюджетного образовательного учреждения высшего образования «Оренбургский государственный университет» Зыкова Г. Показать больше

Министерство науки и высшего образования Российской Федерации Орский гуманитарно-технологический институт (филиал) федерального государственного бюджетного образовательного учреждения высшего образования «Оренбургский государственный университет» Зыкова Г. В., Попов А. С., Сапуглецева Т. Н. ОСНОВЫ ПРОГРАММИРОВАНИЯ НА ЯЗЫКЕ PYTHON Утверждено редакционно-издательским советом Орского гуманитарно-технологического института (филиала) ОГУ в качестве учебно-методического пособия Орск 2019 1 Спрятать

  • Похожие публикации
  • Поделиться
  • Код вставки
  • Добавить в избранное
  • Комментарии

Б. Керниган, Д. Ритчи

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

Многие важные идеи Си взяты из языка BCPL, автором которого является Мартин Ричардс. Влияние BCPL на Си было косвенным — через язык B, разработанный Кеном Томпсоном в 1970 г. для первой системы UNIX, реализованной на PDP-7.

BCPL и B — «бестиповые» языки. В отличие от них Си обеспечивает разнообразие типов данных. Базовыми типами являются символы, а также целые и числа с плавающей точкой различных размеров. Кроме того, имеется возможность получать целую иерархию производных типов данных из указателей, массивов, структур и объединений. Выражения формируются из операторов и операндов. Любое выражение, включая присваивание и вызов функции, может быть инструкцией. Указатели обеспечивают машинно-независимую адресную арифметику.

В Си имеются основные управляющие конструкции, используемые в хорошо структурированных программах: составная инструкция (<. . .>), ветвление по условию (if-else), выбор одной альтернативы из многих (switch), циклы с проверкой наверху (while, for) и с проверкой внизу (do), а также средство прерывания цикла (break).

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

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

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

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

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

Отсутствие некоторых из перечисленных средств может показаться серьезным недостатком («выходит, чтобы сравнить две строки символов, нужно обращаться к функции?»). Однако компактность языка имеет реальные выгоды. Поскольку Си относительно мал, то и описание его кратко, и овладеть им можно быстро. Программист может реально рассчитывать на то, что он будет знать, понимать и на практике регулярно пользоваться всеми возможностями языка.

В течение многих лет единственным определением языка Си было первое издание книги «Язык программирования Си». В 1983 г. Институтом американских национальных стандартов (ANSI) учреждается комитет для выработки современного исчерпывающего определения языка Си. Результатом его работы явился стандарт для Си («ANSI-C»), выпущенный в 1988 г. Большинство положений этого стандарта уже учтено в современных компиляторах.

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

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

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

Второй значительный вклад стандарта — это определение библиотеки, поставляемой вместе с Си- компилятором, в которой специфицируются функции доступа к возможностям операционной системы (например чтения-записи файлов), форматного ввода-вывода, динамического выделения памяти, манипуляций со строками символов и т. д. Набор стандартных заголовочных файлов обеспечивает единообразный доступ к объявлениям функций и типов данных. Гарантируется, что программы, использующие эту библиотеку при взаимодействии с операционной системой, будут работать также и на других машинах. Большинство программ, составляющих библиотеку, созданы по образу и подобию «стандартной библиотеки ввода-вывода» системы UNIX. Эта библиотека описана в первом издании книги и широко используется в других системах. И здесь программисты не заметят существенных различий. Так как типы данных и управляющих структур языка Си поддерживаются командами большинства существующих машин, исполнительная система (run-time library), обеспечивающая независимый запуск и выполнение программ, очень мала. Обращения к библиотечным функциям пишет сам программист (не компилятор), поэтому при желании их можно легко заменить на другие. Почти все программы, написанные на Си, если они не касаются каких-либо скрытых в операционной системе деталей, переносимы на другие машины.

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

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

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

Книга имеет следующую структуру. Глава 1 представляет собой обзор основных средств языка Си. Ее назначение — побудить читателя по возможности быстрее приступить к программированию, так как мы убеждены, что единственный способ изучить новый язык — это писать на нем программы. Эта часть книги предполагает наличие знаний по основным элементам программирования. Никаких пояснений того, что такое компьютер, компиляция или что означает выражение вида n=n+1 не дается. Хотя мы и пытались там, где это возможно, показать полезные приемы программирования, эта книга не призвана быть справочником ни по работе со структурами данных, ни по алгоритмам: когда оказывалось необходимым выбрать, на что сделать ударение, мы предпочитали сконцентрировать внимание на языке.

В главах 2-6 различные средства языка обсуждаются более подробно и несколько более формально, чем в главе 1; при этом по-прежнему упор делается на примеры, являющиеся законченными программами, а не изолированными фрагментами. Глава 2 знакомит с базовыми типами данных, с операторами и выражениями. В главе 3 рассматриваются средства управления последовательностью вычислений: if-else, switch, while, for и т.д. В главе 4 речь идет о функциях и структуре программы (внешних переменных, правилах видимости, делении программы на несколько исходных файлов и т. д.), а также о препроцессоре. В главе 5 обсуждаются указатели и адресная арифметика. Глава 6 посвящена структурам и объединениям.

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

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

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

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

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

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

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

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

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

Напечатать слова Hello, world

Вот первое препятствие, и чтобы его преодолеть, вы должны суметь где-то создать текст программы, успешно его скомпилировать, загрузить, запустить на выполнение и разобраться, куда будет отправлен результат. Как только вы овладеете этим, все остальное окажется относительно просто. Си-программа, печатающая “Hello, world”, выглядит так:

#include

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

cc hello.c

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

a.out
Hello, world

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

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

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

#include

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

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

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

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

printf("Hello, world\n");

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

Последовательность символов в двойных кавычках, такая как «Hello, world\n», называется строкой символов, или строковой константой. Пока что в качестве аргументов для printf и других функций мы будем использовать только строки символов.

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

printf("Hello, world ");

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

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

#include main()

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

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

Упражнение 1.1. Выполните программу, печатающую “Hello, world”, в вашей системе. Поэкспериментируйте, удаляя некоторые части программы, и посмотрите, какие сообщения об ошибках вы получите.

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

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

Приведенная ниже программа выполняет вычисления по формуле °С = (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 /* печать таблицы температур по Фаренгейту и Цельсию для fahr = 0, 20, . 300 */ main() < int fahr, celsius; int lower, upper, step; lower = 0; /* нижняя граница таблицы температур */ upper = 300; /* верхняя граница */ step = 20; /* шаг */ fahr = lower; while (fahr >
/* печать таблицы температур по Фаренгейту и Цельсию для 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

Он работает следующим образом. Проверяется условие в скобках. Если оно истинно (значение 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, выполняет табуляцию (\t) и печатает целое 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°С, а не -17. Чтобы получить более точные значения температур, нам надо пользоваться не целочисленной арифметикой, а арифметикой с плавающей точкой. Это потребует некоторых изменений в программе.

#include /* печать таблицы температур по Фаренгейту и Цельсию для fahr = 0, 20, . . 300; вариант с плавающей точкой */ main() < float fahr, celsius; int lower, upper, step; lower = 0; /* нижняя граница таблицы температур */ upper = 300; /* верхняя граница */ step = 20; /* шаг */ fahr = lower; while (fahr >

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

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

fahr=lower;
while(fahr 

работают естественным образом, т. е. перед выполнением операции значение int приводится к float.

Спецификация %3.0f в printf определяет печать числа с плавающей точкой (в данном случае числа fahr) в поле шириной не более трех позиций без десятичной точки и дробной части. Спецификация %6.1f описывает печать другого числа (celsius) в поле из шести позиций с одной цифрой после десятичной точки. Напечатано будет следующее:

0 -17.8 20 -6.7 40 4.4

Ширину и точность можно не задавать; %6f означает, что число будет занимать не более шести позиций; %.2f - число имеет две цифры после десятичной точки, но ширина не ограничена; %f просто указывает на печать числа с плавающей точкой.

%d - печать десятичного целого.
%6d - печать десятичного целого в поле из шести позиций.
%f - печать числа с плавающей точкой.
%6f – печать числа с плавающей точкой в поле из шести позиций.
%.2f – печать числа с плавающей точкой с двумя цифрами после десятичной точки.
%6.2f - печать числа с плавающей точкой и двумя цифрами после десятичной точки в поле из шести позиций.

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

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

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

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

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

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

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

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

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

fahr = 0

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

fahr 

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

fahr = fahr + 20

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

Выбор между while и for определяется соображениями ясности программы. Цикл 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 

Величины 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 /* копирование ввода на вывод, 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 - целая константа, определенная в . Какое значение имеет эта константа - неважно, лишь бы оно отличалось от любого из возможных значений типа char. Использование именованной константы с унифицированным именем гарантирует, что программа не будет зависеть от конкретного числового значения, которое, возможно, в других Си-системах будет иным.

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

c = getchar()

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

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

Цикл 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 /* подсчет вводимых символов; 1-я версия */ main()

представляет новый оператор ++, который означает увеличить на единицу. Вместо этого можно было бы написать 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()

В printf спецификатор %f применяется как для float, так и для double; спецификатор %.0f означает печать без десятичной точки и дробной части (последняя в нашем случае отсутствует).

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

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

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

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

#include /* подсчет строк входного потока */ main()

Тело цикла теперь образует инструкция 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 #define IN 1 /* внутри слова */ #define OUT 0 /* вне слова */ /* подсчет строк, слов и символов */ main() < int с, nl, nw, nc, state; state = OUT; nl = nw = nc = 0; while ((с = 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 /* подсчет цифр, символов-разделителей и прочих символов */ main() < int с, 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 3 0 0 0 0 0 0 0 1, символы-разделители = 123, прочие = 345
int ndigit[10];

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

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

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

if (c >= '0' && c 

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

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

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

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

if (c >= '0' && c 
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 Функции

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

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

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

#include 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)

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

тип-результата имя-функции (список параметров, если он есть) < объявления инструкции >

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

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

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

При каждом вызове функции power передаются два аргумента, и каждый раз главная программа main в ответ получает целое число, которое затем приводится к должному формату и печатается. Внутри выражения 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 ожидает двух аргументов типа int и возвращает результат типа int. Это объявление, называемое прототипом функции, должно быть согласовано с определением и всеми вызовами power. Если определение функции или вызов не соответствует своему прототипу, это ошибка.

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

int power(int, int);

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

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

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

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

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

int power();

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

Новый синтаксис для прототипов функций облегчает компилятору обнаружение ошибок в количестве аргументов и их типах. Старый синтаксис объявления и определения функции все еще допускается стандартом 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 #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 && с != '\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[])

Мы предполагаем, что функции getline и copy, описанные в начале программы, находятся в том же файле, что и main.

Функции main и getline взаимодействуют между собой через пару аргументов и возвращаемое значение. В getline аргументы определяются строкой

int getline(char s[], int lim);

Как мы видим, ее первый аргумент s есть массив, а второй, lim, имеет тип int. Задание размера массива в определении имеет целью резервирование памяти. В самой getline задавать длину массива s нет необходимости, так как его размер указан в main. Чтобы вернуть значение вызывающей программе, getline использует return точно так же, как это делает функция power. В приведенной строке также сообщается, что getline возвращает значение типа int, но так как при отсутствии указания о типе подразумевается int, то перед getline слово int можно опустить.

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

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

“hello\n”

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

h е l l o \n \0

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

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

Пользователи 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 #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)

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

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

Если же программа расположена в нескольких исходных файлах и внешняя переменная определена в файле1, а используется в файле2 и файлеЗ, то объявления extern в файле2 и файлеЗ обязательны, поскольку необходимо указать, что во всех трех файлах функции обращаются к одной и той же внешней переменной. На практике обычно удобно собрать все объявления внешних переменных и функций в отдельный файл, называемый заголовочным (header -файлом), и помещать его с помощью #include в начало каждого исходного файла. В именах header-файлов по общей договоренности используется суффикс .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. Напишите программу, проверяющую Си-программы на элементарные синтаксические ошибки вроде несбалансированности скобок всех видов. Не забудьте о кавычках (одиночных и двойных), эскейп-последовательностях (\. ) и комментариях. (Это сложная программа, если писать ее для общего случая.)

Глава 2. Типы, операторы и выражения

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

Стандартом ANSI было утверждено значительное число небольших изменений и добавлений к основным типам и выражениям. Любой целочисленный тип теперь может быть со знаком, signed, и без знака, unsigned. Предусмотрен способ записи беззнаковых констант и шестнадцатеричных символьных констант. Операции с плавающей точкой допускаются теперь и с одинарной точностью. Введен тип long double, обеспечивающий повышенную точность. Строковые константы конкатенируются ("склеиваются”) теперь во время компиляции. Частью языка стали перечисления (enum), формализующие для типа установку диапазона значений. Объекты для защиты их от каких-либо изменений разрешено помечать как const. В связи с введением новых типов расширены правила автоматического преобразования из одного арифметического типа в другой.

2.1 Имена переменных

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

Имена составляются из букв и цифр; первым символом должна быть буква. Символ подчеркивания "_" считается буквой; его иногда удобно использовать, чтобы улучшить восприятие длинных имен переменных. Не начинайте имена переменных с подчеркивания, так как многие переменные библиотечных программ начинаются именно с этого знака. Большие (прописные) и малые (строчные) буквы различаются, так что x и X - это два разных имени. Обычно в программах на Си малыми буквами набирают переменные, а большими - именованные константы.

Для внутренних имен значимыми являются первые 31 символ. Для имен функций и внешних переменных число значимых символов может быть меньше 31, так как эти имена обрабатываются ассемблерами и загрузчиками и языком не контролируются. Уникальность внешних имен гарантируется только в пределах 6 символов, набранных безразлично в каком регистре. Ключевые слова if, else, int, float и т. д. зарезервированы, и их нельзя использовать в качестве имен переменных. Все они набираются на нижнем регистре (т. е. малыми буквами).

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

2.2 Типы и размеры данных

В Си существует всего лишь несколько базовых типов:

char - единичный байт, который может содержать один символ из допустимого символьного набора;
int - целое, обычно отображающее естественное представление целых в машине;
float - число с плавающей точкой одинарной точности;
double - число с плавающей точкой двойной точности.

Имеется также несколько квалификаторов, которые можно использовать вместе с указанными базовыми типами. Например, квалификаторы short (короткий) и long (длинный) применяются к целым:

short int sh; long int counter;

В таких объявлениях слово int можно опускать, что обычно и делается. Если только не возникает противоречий со здравым смыслом, short int и long int должны быть разной длины, а int соответствовать естественному размеру целых на данной машине. Чаще всего для представления целого, описанного с квалификатором short, отводится 16 бит, с квалификатором long - 32 бита, а значению типа int - или 16, или 32 бита. Разработчики компилятора вправе сами выбирать подходящие размеры, сообразуясь с характеристиками своего компьютера и соблюдая следующие ограничения: значения типов short и int представляются по крайней мере 16 битами; типа long - по крайней мере 32 битами; размер short не больше размера int, который в свою очередь не больше размера long.

Квалификаторы signed (со знаком) или unsigned (без знака) можно применять к типу char и любому целочисленному типу. Значения unsigned всегда положительны или равны нулю и подчиняются законам арифметики по модулю 2 n , где n - количество бит в представлении типа. Так, если значению char отводится 8 битов, то unsigned char имеет значения в диапазоне от 0 до 255, a signed char – от -128 до 127 (в машине с двоичным дополнительным кодом). Являются ли значения типа просто char знаковыми или беззнаковыми, зависит от реализации, но в любом случае коды печатаемых символов положительны.

Тип long double предназначен для арифметики с плавающей точкой повышенной точности. Как и в случае целых, размеры объектов с плавающей точкой зависят от реализации; float, double и long double могут представляться одним размером, а могут - двумя или тремя разными размерами.

Именованные константы для всех размеров вместе с другими характеристиками машины и компилятора содержатся в стандартных заголовочных файлах и (см. приложение B).

Упражнение 2.1. Напишите программу, которая будет выдавать диапазоны значений типов char, short, int и long, описанных как signed и как unsigned, с помощью печати соответствующих значений из стандартных заголовочных файлов и путем прямого вычисления. Определите диапазоны чисел с плавающей точкой различных типов. Вычислить эти диапазоны сложнее.

2.3 Константы

Целая константа, например 1234, имеет тип int. Константа типа long завершается буквой l или L, например 123456789L: слишком большое целое, которое невозможно представить как int, будет представлено как long. Беззнаковые константы заканчиваются буквой u или U, а окончание ul или UL говорит о том, что тип константы - unsigned long.

Константы с плавающей точкой имеют десятичную точку (123.4), или экспоненциальную часть (1е-2), или же и то и другое. Если у них нет окончания, считается, что они принадлежат к типу double. Окончание f или F указывает на тип float, а l или L - на тип long double.

Целое значение помимо десятичного может иметь восьмеричное или шестнадцатеричное представление. Если константа начинается с нуля, то она представлена в восьмеричном виде, если с 0x или с 0X, то - в шестнадцатеричном. Например, десятичное целое 31 можно записать как 037 или как 0X1F. Записи восьмеричной и шестнадцатеричной констант могут завершаться буквой L (для указания на тип long) и U (если нужно показать, что константа беззнаковая). Например, константа 0XFUL имеет значение 15 и тип unsigned long.

Символьная константа есть целое, записанное в виде символа, обрамленного одиночными кавычками, например 'x'. Значением символьной константы является числовой код символа из набора символов на данной машине. Например, символьная константа '0' в кодировке ASCII имеет значение 48, которое никакого отношения к числовому значению 0 не имеет. Когда мы пишем '0' , а не какое-то значение (например 46), зависящее от способа кодировки, мы делаем программу независимой от частного значения кода, к тому же она и легче читается. Символьные константы могут участвовать в операциях над числами точно так же, как и любые другие целые, хотя чаще они используются для сравнения с другими символами.

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

где ооо - одна, две или три восьмеричные цифры (0 … 7) или

где hh - одна, две или более шестнадцатеричные цифры (0. 9, а. f, A. F). Таким образом, мы могли бы написать

#define VTAB '013' /* вертикальная табуляция в ASCII */ #define BELL '\007' /* звонок в ASCII */

или в шестнадцатеричном виде:

#define VTAB '\xb' /* вертикальная табуляций в ASCII */ #define BELL '\x7' /* звонок в ASCII */

Полный набор эскейп-последовательностей таков:

 сигнал-звонок \b возврат-на-шаг (забой) \f перевод-страницы \n новая-строка \r возврат-каретки \t горизонтальная-табуляция \v вертикальная-табуляция \\ обратная наклонная черта \? знак вопроса \' одиночная кавычка \" двойная кавычка \ooo восьмеричный код \xhh шестнадцатеричный код

Символьная константа '\0' - это символ с нулевым значением, так называемый символ null. Вместо просто 0 часто используют запись '\0', чтобы подчеркнуть символьную природу выражения, хотя и в том и другом случае запись обозначает нуль.

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

#define MAXLINE 1000 char line[MAXLINE+1];
#define LEAP 1 /* in leap years - в високосные годы */ int days[31+28+LEAP+31+30+31+30+31+31+30+31+30+31];

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

"Я строковая константа”
"" /* пустая строка */

Кавычки не входят в строку, а служат только ее ограничителями. Так же, как и в символьные константы, в строки можно включать эскейп-последовательности; \", например, представляет собой двойную кавычку. Строковые константы можно конкатенировать ("склеивать”) во время компиляции; например, запись двух строк

"Здравствуй," " мир!"

эквивалентна записи одной следующей строки:

"Здравствуй, мир!"

Указанное свойство позволяет разбивать длинные строки на части и располагать эти части на отдельных строчках.

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

/* strlen: возвращает длину строки s */ int strlen(char s[])

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

Будьте внимательны и помните, что символьная константа и строка, содержащая один символ, не одно и то же: 'x' не то же самое, что "x". Запись 'x' обозначает целое значение, равное коду буквы x из стандартного символьного набора, а запись "x" - массив символов, который содержит один символ (букву x) и '\0'.

В Си имеется еще один вид константы - константа перечисления. Перечисление - это список целых констант, как, например, в

enum boolean ;

Первое имя в enum имеет значение 0, следующее - 1 и т.д. (если для значений констант не было явных спецификаций). Если не все значения специфицированы, то они продолжают прогрессию, начиная от последнего специфицированного значения, как в следующих двух примерах:

enum escapes < BELL = '\a', BACKSPACE = '\b', TAB = '\t', NEWLINE = '\n', VTAB = '\v', RETURN = '\r'>; enum months < JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC>; /* FEB есть 2, MAR есть 3 и т.д. */

Имена в различных перечислениях должны отличаться друг от друга. Значения внутри одного перечисления могут совпадать.

Средство enum обеспечивает удобный способ присвоить константам имена, причем в отличие от #define значения констант при этом способе могут генерироваться автоматически. Хотя разрешается объявлять переменные типа enum, однако компилятор не обязан контролировать, входят ли присваиваемые этим переменным значения в их тип. Но сама возможность такой проверки часто делает enum лучше, чем #define. Кроме того, отладчик получает возможность печатать значения переменных типа enum в символьном виде.

2.4 Объявления

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

int lower, upper, step; char с, line[1000];

Переменные можно распределять по объявлениям произвольным образом, так что указанные выше списки можно записать и в следующем виде:

int lower; int upper; int step; char c; char line[1000];

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

В своем объявлении переменная может быть инициализирована, как, например:

char esc = '\\'; int i = 0; int limit = MAXLINE+1; float eps = 1.0e-5;

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

К любой переменной в объявлении может быть применен квалификатор const для указания того, что ее значение далее не будет изменяться.

const double е = 2.71828182845905; const char msg[] = "предупреждение: ";

Применительно к массиву квалификатор const указывает на то, что ни один из его элементов не будет меняться. Указание const можно также применять к аргументу- массиву, чтобы сообщить, что функция не изменяет этот массив:

int strlen(const char[]);

Реакция на попытку изменить переменную, помеченную квалификатором const зависит от реализации компилятора.

2.5 Арифметические операторы

Бинарными (т. е. с двумя операндами) арифметическими операторами являются +, -, *, /, а также оператор деления по модулю %. Деление целых сопровождается отбрасыванием дробной части, какой бы она ни была. Выражение

x % y

дает остаток от деления x на y и, следовательно, нуль, если x делится на y нацело. Например, год является високосным, если он делится на 4, но не делится на 100. Кроме того, год является високосным, если он делится на 400. Следовательно,

if ((year % 4 == 0 && year % 100 !=0 || year % 400 == 0) printf("%d високосный год\n", year); else printf("%d невисокосный год\n", year);

Оператор % к операндам типов float и double не применяется. В какую сторону (в сторону увеличения или уменьшения числа) будет усечена дробная часть при выполнении / и каким будет знак результата операции % с отрицательными операндами, зависит от машины.

Бинарные операторы + и - имеют одинаковый приоритет, который ниже приоритета операторов *, / и %, который в свою очередь ниже приоритета унарных операторов + и -. Арифметические операции одного приоритетного уровня выполняются слева направо.

В конце этой главы (параграф 2.12) приводится таблица 2.1,в которой представлены приоритеты всех операторов и очередность их выполнения.

2.6 Операторы отношения и логические операторы

Операторами отношения являются

Все они имеют одинаковый приоритет. Сразу за ними идет приоритет операторов сравнения на равенство:

Операторы отношения имеют более низкий приоритет, чем арифметические, поэтому выражение вроде i

2.7 Преобразования типов

Если операнды оператора принадлежат к разным типам, то они приводятся к некоторому общему типу. Приведение выполняется в соответствии с небольшим числом правил. Обычно автоматически производятся лишь те преобразования, которые без какой-либо потери информации превращают операнды с меньшим диапазоном значений в операнды с большим диапазоном, как, например, преобразование целого в число с плавающей точкой в выражении вроде f+i. Выражения, не имеющие смысла, например число с плавающей точкой в роли индекса, не допускаются. Выражения, в которых могла бы теряться информация (скажем, при присваивании длинных целых переменным более коротких типов или при присваивании значений с плавающей точкой целым переменным), могут повлечь за собой предупреждение, но они допустимы.

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

/* atoi: преобразование s в целое */ int atoi(char s[]) < int i, n; n = 0; for (i = 0; s[i] >= '0' && s[i]

Как мы уже говорили в главе 1, выражение

s[i] -'0'

дает числовое значение символа, хранящегося в s[i], так как значения '0', '1' и пр. образуют непрерывную возрастающую последовательность.

Другой пример приведения char к int связан с функцией lower, которая одиночный символ из набора ASCII, если он является заглавной буквой, превращает в строчную. Если же символ не является заглавной буквой, lower его не изменяет.

/* lower: преобразование c в строчную, только для ASCII */ int lower(int c) < if (c >= 'A' && c

В случае ASCII эта программа будет работать правильно, потому что между одноименными буквами верхнего и нижнего регистров - одинаковое расстояние (если их рассматривать как числовые значения). Кроме того, латинский алфавит - плотный, т. е. между буквами A и Z расположены только буквы. Для набора EBCDIC последнее условие не выполняется, и поэтому наша программа в этом случае будет преобразовывать не только буквы.

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

c >= '0' && c 

можно заменить на isdigit(c)

Далее мы будем пользоваться функциями из .

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

Гарантируется, что любой символ из стандартного набора печатаемых символов никогда не будет отрицательным числом, поэтому в выражениях такие символы всегда являются положительными операндами. Но произвольный восьмибитовый код в переменной типа char на одних машинах может быть отрицательным числом, а на других - положительным. Для совместимости переменные типа char, в которых хранятся несимвольные данные, следует специфицировать явно как signed или unsigned.

Отношения вроде i > j и логические выражения, перемежаемые операторами && и ||, определяют выражение-условие, которое имеет значение 1, если оно истинно, и 0, если ложно. Так, присваивание

d = c >= '0' && c 

установит d в значение 1, если c есть цифра, и 0 в противном случае. Однако функции, подобные isdigit, в качестве истины могут выдавать любое ненулевое значение. В местах проверок внутри if, while, for и пр. "истина” просто означает "не нуль”.

Неявные арифметические преобразования, как правило, осуществляются естественным образом. В общем случае, когда оператор вроде + или * с двумя операндами (бинарный оператор) имеет разнотипные операнды, прежде чем операция начнет выполняться, "низший” тип повышается до "высшего”. Результат будет иметь высший тип. В параграфе 6 приложения A правила преобразования сформулированы точно. Если же в выражении нет беззнаковых операндов, можно удовлетвориться следующим набором неформальных правил:

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

Правила преобразования усложняются с появлением операндов типа unsigned. Проблема в том, что сравнения знаковых и беззнаковых значений зависят от размеров целочисленных типов, которые на разных машинах могут отличаться. Предположим, что значение типа int занимает 16 битов, а значение типа long - 32 бита. Тогда -1L 1UL, так как -1L повышается до типа unsigned long и воспринимается как большое положительное число.

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

Тип char превращается в int путем распространения знака или другим описанным выше способом.

Тип long int преобразуются в short int или в значения типа char путем отбрасывания старших разрядов. Так, в

int i; char c; i = c; c = i;

значение c не изменится. Это справедливо независимо от того, распространяется знак при переводе char в int или нет. Однако, если изменить очередность присваиваний, возможна потеря информации.

Если x принадлежит типу float, а i - типу int, то и x=i, и i=z вызовут преобразования, причем перевод float в int сопровождается отбрасыванием дробной части. Если double переводится во float, то значение либо округляется, либо обрезается; это зависит от реализации.

Так как аргумент в вызове функции есть выражение, при передаче его функции также возможно преобразование типа. При отсутствии прототипа (функции аргументы тина char и short переводятся в int, a float - в double. Вот почему мы объявляли аргументы типа int или double даже тогда, когда в вызове функции использовали аргументы типа char или float.

И наконец, для любого выражения можно явно ("насильно”) указать преобразование его типа, используя унарный оператор, называемый приведением. Конструкция вида

(имя-типа) выражение 

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

sqrt((double) n)

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

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

double sqrt(double);

перед обращением к sqrt в присваивании

root2 = sqrt(2);

целое 2 будет переведено в значение double 2.0 автоматически без явного указания операции приведения.

Операцию приведения проиллюстрируем на переносимой версии генератора псевдослучайных чисел и функции, инициализирующей "семя”. И генератор, и функция входят в стандартную библиотеку.

unsigned long int next = 1; /* rand: возвращает псевдослучайное целое 0. 32767 */ int rand(void) < next = next * 1103515245 + 12345; return (unsigned int)(next/65536) % 32768; >/* srand: устанавливает "семя” для rand() */ void srand(unsigned int seed)

Упражнение 2.3. Напишите функцию htol(s), которая преобразует последовательность шестнадцатеричных цифр, начинающуюся с 0x или 0X, в соответствующее целое. Шестнадцатеричными цифрами являются символы 0. 9, a. f, А. F.

2.8 Операторы инкремента и декремента

В Си есть два необычных оператора, предназначенных для увеличения и уменьшения переменных. Оператор инкремента ++ добавляет 1 к своему операнду, а оператор декремента -- вычитает 1. Мы уже неоднократно использовали ++ для наращивания значения переменных, как, например, в

if (c == '\n') ++nl;

Необычность операторов ++ и -- в том, что их можно использовать и как префиксные (помещая перед переменной: ++n), и как постфиксные (помещая после переменной: n++) операторы. В обоих случаях значение n увеличивается на 1, но выражение ++n увеличивает n до того, как его значение будет использовано, а n++ - после того. Предположим, что n содержит 5, тогда

x = n++;

установит x в значение 5, а

x = ++n;

установит x в значение 6. И в том и другом случае n станет равным 6. Операторы инкремента и декремента можно применять только к переменным. Выражения вроде (i+j)++ недопустимы.

Если требуется только увеличить или уменьшить значение переменной (но не получить ее значение), как например

if (c=='\n') nl++;

то безразлично, какой оператор выбрать - префиксный или постфиксный. Но существуют ситуации, когда требуется оператор вполне определенного типа. Например, рассмотрим функцию squeeze(s, c), которая удаляет из строки s все символы, совпадающие с c:

/* squeeze: удаляет все c из s*/ void squeeze(char s[], int с)

Каждый раз, когда встречается символ, отличный от c, он копируется в текущую j-ю позицию, и только после этого переменная j увеличивается на 1, подготавливаясь таким образом к приему следующего символа. Это в точности совпадает со следующими действиями:

if (s[i] != с)

Другой пример - функция getline, которая нам известна по главе 1. Приведенную там запись

if (c =='\n')

можно переписать более компактно:

if (с == '\n') s[i++] = с;

В качестве третьего примера рассмотрим стандартную функцию strcat(s,t), которая строку t помещает в конец строки s. Предполагается, что в s достаточно места, чтобы разместить там суммарную строку. Мы написали strcat так, что она не возвращает никакого результата. На самом деле библиотечная strcat возвращает указатель на результирующую строку.

/* strcat: помещает t в конец s; s достаточно велика */ void strcat (char s[], char t[]) < int i, j; i = j = 0; while (s[i] != '\0') /* находим конец s */ i++; while ((s[i++] = t[j++]) != '\0') /* копируем t */ ; >

При копировании очередного символа из t в s постфиксный оператор ++ применяется и к i, и к j, чтобы на каждом шаге цикла переменные i и j правильно отслеживали позиции перемещаемого символа.

Упражнение 2.4. Напишите версию функции squeeze(s1,s2), которая удаляет из s1 все символы, встречающиеся в строке s2.

Упражнение 2.5. Напишите функцию any(s1,s2), которая возвращает либо ту позицию в s1, где стоит первый символ, совпавший с любым из символов в s2, либо -1 (если ни один символ из s1 не совпадает с символами из s2). (Стандартная библиотечная функция strpbrk делает то же самое, но выдает не номер позиции символа, а указатель на символ.)

2.9 Побитовые операторы

В Си имеются шесть операторов для манипулирования с битами. Их можно применять только к целочисленным операндам, т. е. к операндам типов char, short, int и long, знаковым и беззнаковым.

& - побитовое И | - побитовое ИЛИ ^ - побитовое исключающее ИЛИ. > - сдвиг вправо. ~ - побитовое отрицание (унарный).

Оператор & (побитовое И) часто используется для обнуления некоторой группы разрядов. Например

n = n & 0177;

обнуляет в n все разряды, кроме младших семи.

Оператор | (побитовое ИЛИ) применяют для установки разрядов; так,

x = x | SET_ON;

устанавливает единицы в тех разрядах x, которым соответствуют единицы в SET_ON.

Оператор ^ (побитовое исключающее ИЛИ) в каждом разряде установит 1, если соответствующие разряды операндов имеют различные значения, и 0, когда они совпадают.

Поразрядные операторы & и | следует отличать от логических операторов && и ||, которые при вычислении слева направо дают значение истинности. Например, если x равно 1, а y равно 2, то x & y даст нуль, а x && y - единицу.

Операторы > сдвигают влево или вправо свой левый операнд на число битовых позиций, задаваемое правым операндом, который должен быть неотрицательным. Так, x > (р+1-n) сдвигает нужное нам поле к правому краю. Константа ~0 состоит из одних единиц, и ее сдвиг влево на n бит (~0

2.10 Операторы и выражения присваивания

i = i + 2;

в котором стоящая слева переменная повторяется и справа, можно написать в сжатом виде:

i += 2;

Оператор +=, как и =, называется оператором присваивания.

Большинству бинарных операторов (аналогичных + и имеющих левый и правый операнды) соответствуют операторы присваивания op=, где op - один из операторов

Если выр1 и выр2 - выражения, то

выр1 op= выр2 
выр1 = (выр1) op (выр2)

с той лишь разницей, что выр1 вычисляется только один раз. Обратите внимание на скобки вокруг выр2:

x *= y + 1
x = x * (y + 1)
x=x*y+1

В качестве примера приведем функцию bitcount, подсчитывающую число единичных битов в своем аргументе целочисленного типа.

/* bitcount: подсчет единиц в х */ int bitcount(unsigned х) < int b; for (b = 0; х != 0; x >>= 1) if (x & 01) b++; return b; >

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

Помимо краткости операторы присваивания обладают тем преимуществом, что они более соответствуют тому, как человек мыслит. Мы говорим "прибавить 2 к i" или "увеличить i на 2", а не "взять i, добавить 2 и затем вернуть результат в i", так что выражение i+=2 лучше, чем i=i+2. Кроме того, в сложных выражениях вроде

yyval[yypv[p3+p4] + yypv[p1+p2]]+= 2

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

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

while ((с = getchar()) != EOF)

В выражениях встречаются и другие операторы присваивания (+=, -= и т. д.), хотя и реже. Типом и значением любого выражения присваивания являются тип и значение его левого операнда после завершения присваивания.

Упражнение 2.9. Применительно к числам, в представлении которых использован дополнительный код, выражение x &= (x-1) уничтожает самую правую 1 в x. Объясните, почему. Используйте это наблюдение при написании более быстрого варианта функции bitcount.

2.11 Условные выражения

if (a > b) z = a; else z = b;

пересылают в z большее из двух значений a и b. Условное выражение, написанное с помощью тернарного (т. е. имеющего три операнда) оператора "? : ", представляет собой другой способ записи этой и подобных ей конструкций. В выражении

выр1 ? выр2 : выр3 

первым вычисляется выражение выр1. Если его значение не нуль (истина), то вычисляется выражение выр2, и значение этого выражения становится значением всего условного выражения. В противном случае вычисляется выражение выр3 и его значение становится значением условного выражения. Следует отметить, что из выражений выр2 и выр3 вычисляется только одно из них. Таким образом, чтобы установить в z большее из a и b, можно написать

z = (a > b) ? a : b; /* z = max(a, b) */

Следует заметить, что условное выражение и в самом деле является выражением, и его можно использовать в любом месте, где допускается выражение. Если выр2 и выр3 принадлежат разным типам, то тип результата определяется правилами преобразования, о которых шла речь в этой главе ранее. Например, если f имеет тип float, а n - тип int, то типом выражения

(n > 0) ? f : n

будет float вне зависимости от того, положительно значение n или нет.

Заключать в скобки первое выражение в условном выражении не обязательно, так как приоритет ?: очень низкий (более низкий приоритет имеет только присваивание), однако мы рекомендуем всегда это делать, поскольку благодаря обрамляющим скобкам условие в выражении лучше воспринимается.

Условное выражение часто позволяет сократить программу. В качестве примера приведем цикл, обеспечивающий печать n элементов массива по 10 на каждой строке с одним про6елом между колонками; каждая строка цикла, включая последнюю, заканчивается символом новой строки:

for (i = 0; i < n; i++) printf("%6d %c", a[i], (i%10 == 9 || i == n-1) ? '\n' : ' ');

Символ новой строки посылается после каждого десятого и после n-го элемента. За всеми другими элементами следует пробел. Эта программа выглядит довольно замысловато, зато она более компактна, чем эквивалентная программа с использованием if-else. Вот еще один хороший пример :

printf("Вы имеете %d элемент%s: \n", n, (n%10 == 1 && n%100 != 11) ? " " : ((n%100 < 10 || n%100 >20) && n%10 >= 2 && n%10 

Упражнение 2.10. Напишите функцию lower, которая переводит большие буквы в малые, используя условное выражение (а не конструкцию if-else).

2.12 Приоритет и очередность вычислений

В таблице 2.1 показаны приоритеты и очередность вычислений всех операторов, включая и те, которые мы еще не рассматривали. Операторы, перечисленные на одной строке, имеют одинаковый приоритет: строки упорядочены по убыванию приоритетов; так, например, *, / и % имеют одинаковый приоритет, который выше, чем приоритет бинарных + и -. "Оператор” () относится к вызову функции. Операторы -> и . (точка) обеспечивают доступ к элементам структур; о них пойдет речь в главе 6, там же будет рассмотрен и оператор sizeof (размер объекта). Операторы * (косвенное обращение по указателю) и & (получение адреса объекта) обсуждаются в главе 5. Оператор "запятая” будет рассмотрен в главе 3.

Таблица 2.1. Приоритеты и очередность вычислений операторов

Операторы Выполняются
() [] -> . слева направо
! ~ ++ -- + - * & (type) sizeof справа налево
* / % слева направо
+ - слева направо
> слева направо
>= слева направо
== != слева направо
& слева направо
^ слева направо
| слева направо
&& слева направо
|| слева направо
?: справа налево
= += -= *= /= %= &= ^= |= >= справа налево
, слева направо

Примечание. Унарные операторы +, -, * и & имеют более высокий приоритет, чем те же бинарные операторы.

Заметим, что приоритеты побитовых операторов &, ^ и | ниже, чем приоритет == и != , из-за чего в побитовых проверках, таких как

if ((x & MASK) == 0) .

чтобы получить правильный результат, приходится использовать скобки. Си подобно многим языкам не фиксирует очередность вычисления операндов оператора (за исключением &&, ||, ?: и ,). Например, в инструкции вида

x = f() + g();

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

Очередность вычисления аргументов функции также не определена, поэтому на разных компиляторах

printf("%d %d\n", ++n, power(2, n)); /* НЕВЕРНО*/

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

++n; printf("%d %d\n", n, power(2, n));

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

a[i] = i++; /* I.B.: doubtful example */

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

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

Глава 3. Управление

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

3.1 Инструкции и блоки

Выражение, скажем x = 0, или i++, или printf(…), становится инструкцией, если в конце его поставить точку с запятой, например:

x = 0; i++; printf(. );

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

Фигурные скобки и > используются для объединения объявлений и инструкций в составную инструкцию, или блок, чтобы с точки зрения синтаксиса эта новая конструкция воспринималась как одна инструкция. Фигурные скобки, обрамляющие группу инструкций, образующих тело функции, - это один пример; второй пример - это скобки, объединяющие инструкции, помещенные после if, else, while или for. (Переменные могут быть объявлены внутри любого блока, об этом разговор пойдет в главе 4.) После правой закрывающей фигурной скобки в конце блока точка с запятой не ставится.

3.2 Конструкция if-else

Инструкция if-else используется для принятия решения. Формально ее синтаксисом является:

if (выражение) инструкция1 else инструкция2 

причем else-часть может и отсутствовать. Сначала вычисляется выражение, и, если оно истинно (т. е. отлично от нуля), выполняется инструкция1. Если выражение ложно (т. е. его значение равно нулю) и существует else-часть, то выполняется инструкция2.

Так как if просто проверяет числовое значение выражения, условие иногда можно записывать в сокращенном виде. Так, запись

if (выражение)
if ( выражение != 0 )

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

Отсутствие else-части в одной из вложенных друг в друга if-конструкций может привести к неоднозначному толкованию записи. Эту неоднозначность разрешают тем, что else связывают с ближайшим if, у которого нет своего else. Например, в

if (n > 0) if (а > b) z = a; else z = b;

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

if (n > 0) < if (а >b) z = a; > else z = b;

Ниже приводится пример ситуации, когда неоднозначность особенно опасна:

if (n >= 0) for (i=0; i < n; i++) if (s[i] >0) < printf ("…"); return i; >else /* НЕВЕРНО */ printf("ошибка – отрицательное n\n");

С помощью отступов мы недвусмысленно показали, что нам нужно, однако компилятор не воспримет эту информацию и отнесет else к внутреннему if. Искать такого рода ошибки особенно тяжело. Здесь уместен следующий совет: вложенные if обрамляйте фигурными скобками. Кстати, обратите внимание на точку с запятой после z = a в

if (а > b) z = а; else z = b;

Здесь она обязательна, поскольку по правилам грамматики за if должна следовать инструкция, а выражение-инструкция вроде z = a; всегда заканчивается точкой с запятой.

3.3 Конструкция else-if

if (выражение) инструкция else if (выражение) инструкция else if (выражение) инструкция else if (выражение) инструкция else инструкция 

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

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

else инструкция 

можно опустить или использовать для фиксации ошибочной ("невозможной") ситуации.

В качестве иллюстрации трехпутевого ветвления рассмотрим функцию бинарного поиска значения x в массиве v. Предполагается, что элементы v упорядочены по возрастанию. Функция выдает положение x в v (число в пределах от 0 до n-1 ), если x там встречается, и -1, если его нет.

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

/* binsearch: найти x в v[0] v[mid]) low = mid+1; else /* совпадение найдено */ return mid; > return –1; /* совпадения нет */ >

Основное действие, выполняемое на каждой шаге поиска, - сравнение значения x (меньше, больше или равно) с элементом v[mid]; это сравнение естественно поручить конструкции else-if.

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

3.4 Переключатель switch

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

switch (выражение) < case конст-выр: инструкции case конст-выр: инструкции default: инструкции >

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

В главе 1 мы написали программу, подсчитывающую число вхождений в текст каждой цифры, символов-разделителей (пробелов, табуляций и новых строк) и всех остальных символов. В ней мы использовали последовательность if. else if. else. Теперь приведем вариант этой программы с переключателем switch:

#include main() /* подсчет цифр, символов-разделителей и прочих символов */ < int c, i, nwhite, nother, ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; i++) ndigit[i] = 0; while ((с = getchar()) != EOF) < switch (c) < case '0' : case '1' : case '2' : case '3' : case '4' : case '5' : case '6' : case '7' : case '8' : case '9' : ndigit[c - '0']++; break; case ' ': case '\n': case '\t': nwhite++; break; default: nother++; break; >> printf ("цифр %d", ndigit[i]); printf(", символов-разделителей = %d, прочих = %d\n", nwhite, nother); return 0; >

Инструкция break вызывает немедленный выход из переключателя switch. Поскольку выбор ветви case реализуется как переход на метку, то после выполнения одной ветви case, если ничего не предпринять, программа провалится вниз на следующую ветвь. Инструкции break и return — наиболее распространенные средства выхода из переключателя. Инструкция break используется также для принудительного выхода из циклов while, for и do-while (мы еще поговорим об этом чуть позже).

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

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

Упражнение 3.2. Напишите функцию escape (s,t), которая при копировании текста из t в s преобразует такие символы, как новая строка и табуляция в "видимые последовательности символов" (вроде \n и \t). Используйте инструкцию switch. Напишите функцию, выполняющую обратное преобразование эскейп- последовательностей в настоящие символы.

3.5 Циклы while и for

Мы уже встречались с циклами while и for. В цикле

while (выражение) инструкция 

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

Инструкция for

for (выр1; выр2; выр3) инструкция 
выр1; while (выр2) < инструкция выр3; >

если не считать отличий в поведении инструкции continue, речь о которой пойдет в параграфе 3.7.

С точки зрения грамматики три компоненты цикла for представляют собой произвольные выражения, но чаще выр1 и выр3 — это присваивания или вызовы функций, а выр2 - выражение отношения. Любое из этих трех выражений может отсутствовать, но точку с запятой опускать нельзя. При отсутствии выр1, или выр3 считается, что их просто нет в конструкции цикла; при отсутствии выр2, предполагается, что его значение как бы всегда истинно. Например,

есть "бесконечный" цикл, выполнение которого, вероятно, прерывается каким-то другим способом, например с помощью инструкций break или return. Какой цикл выбрать: while или for - это дело вкуса. Так, в

while ((c = getchar()) ==' ' || c == '\n' || c == '\t') ; /* обойти символы-разделители */

нет ни инициализации, ни пересчета параметра, поэтому здесь больше подходит while.

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

for (i = 0; i < n; i++) .

Это похоже на DO-циклы в Фортране и for-циклы в Паскале. Сходство, однако, не вполне точное, так как в Си индекс и его предельное значение могут изменяться внутри цикла, и значение индекса i после выхода из цикла всегда определено. Поскольку три компонента цикла могут быть произвольными выражениями, организация for-циклов не ограничивается только случаем арифметической прогрессии. Однако включать в заголовок цикла вычисления, не имеющие отношения к инициализации и инкрементированию, считается плохим стилем. Заголовок лучше оставить только для операций управления циклом.

В качестве более внушительного примера приведем другую версию программы atoi, выполняющей преобразование строки в ее числовой эквивалент. Это более общая версия по сравнению с рассмотренной в главе 2, в том смысле, что она игнорирует левые символы-разделители (если они есть) и должным образом реагирует на знаки + и -, которые могут стоять перед цифрами. (В главе 4 будет рассмотрен вариант atof, который осуществляет подобное преобразование для чисел с плавающей точкой.)

Структура программы отражает вид вводимой информации:

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

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

#include /* atoi: преобразование s в целое число; версия 2 */ int atoi(char s[]) < int i, n, sign; /* игнорировать символы-разделители */ for (i = 0; isspace(s[i]); i++) ; sign = ( s[i] == '-' ) ? -1 : 1; if (s[i] == '+' || s[i] == '-') /* пропуск знака */ i++; for (n = 0; isdigit(s[i]); i++) n = 10 * n + (s[i] - '0'); return sign * n; >

Заметим, что в стандартной библиотеке имеется более совершенная функция преобразования строки в длинное целое (long int)-функция strtol (см. параграф 5 приложения B).

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

/* shellsort: сортируются v[0]. v[n-1] в возрастающем порядке */ void shellsort (int v[], int n) < int gap, i, j, temp; for (gap = n/2; gap >0; gap /= 2) for (i = gap; i < n; i++) for (j = i- gap; j >= 0 && v[j] > v[j+gap]; j -= gap) < temp = v[j]; v[j] = v[j + gap]; v[j + gap] = temp; >>

Здесь использованы три вложенных друг в друга цикла. Внешний управляет интервалом gap между сравниваемыми элементами, сокращая его путем деления пополам от n/2 до нуля. Средний цикл перебирает элементы. Внутренний - сравнивает каждую пару элементов, отстоящих друг от друга на расстоянии gap, и переставляет элементы в неупорядоченных парах. Так как gap обязательно сведется к единице, все элементы в конечном счете будут упорядочены. Обратите внимание на то, что универсальность цикла for позволяет сделать внешний цикл по форме похожим на другие, хотя он и не является арифметической прогрессией.

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

#include /* reverse: переворачивает строку s (результат в s) */ void reverse(char s[]) < int с, i, j; for (i = 0, j = strlen(s)-1; i < j; i++, j--) < с = s[i]; s[i] = s[j]; s[j] = c; >>

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

Запятыми как операторами следует пользоваться умеренно. Более всего они уместны в конструкциях, которые тесно связаны друг с другом (как в for-цикле программы reverse), а также в макросах, в которых многоступенчатые вычисления должны быть выражены одним выражением. Запятой-оператором в программе reverse можно было бы воспользоваться и при обмене символами в проверяемых парах элементов строки, мысля этот обмен как одну отдельную операцию:

for (i = 0, j = strlen(s)-1; i < j; i++, j--) с = s[i], s[i] = s[j], s[j] = c;

Упражнение 3.3. Напишите функцию expand(s1,s2), заменяющую сокращенную запись наподобие a-z в строке s1 эквивалентной полной записью аbс. хуz в s2. В s1 допускаются буквы (прописные и строчные) и цифры. Следует уметь справляться с такими случаями, как a-b-c, a-z0-9 и -a-b. Считайте знак - в начале или в конце s1 обычным символом минус.

3.6 Цикл do-while

Как мы говорили в главе 1, в циклах while и for проверка условия окончания цикла выполняется наверху. В Си имеется еще один вид цикла, do-while, в котором эта проверка в отличие от while и for делается внизу после каждого прохождения тела цикла, т. е. после того, как тело выполнится хотя бы один раз. Цикл do-while имеет следующий синтаксис:

do инструкция while (выражение);

Сначала выполняется инструкция, затем вычисляется выражение. Если оно истинно, то инструкция выполняется снова и т. д. Когда выражение становится ложным, цикл заканчивает работу. Цикл do-while эквивалентен циклу repeat-until в Паскале с той лишь разницей, что в первом случае указывается условие продолжения цикла, а во втором — условие его окончания.

Опыт показывает, что цикл do-while используется гораздо реже, чем while и for. Тем не менее потребность в нем время от времени возникает, как, например, в функции itoa (обратной по отношению к atoi), преобразующей число в строку символов. Выполнить такое преобразование оказалось несколько более сложным делом, чем ожидалось, поскольку простые алгоритмы генерируют цифры в обратном порядке. Мы остановились на варианте, в котором сначала формируется обратная последовательность цифр, а затем она реверсируется.

/* itoa: преобразование n в строку s */ void itoa(int n, char s[]) < int i, sign; if ((sign = n) < 0) /* сохраняем знак */ n =-n; /* делаем n положительным */ i = 0; do < /* генерируем цифры в обратном порядке */ s[i++] = n %10 + '0'; /* следующая цифра */ >while ((n /= 10) > 0); /* исключить ее */ if (sign

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

Упражнение 3.4. При условии, что для представления чисел используется дополнительный код, наша версия itoa не справляется с самым большим по модулю отрицательным числом, значение которого равняется -(2 n-1 ), где n - размер слова. Объясните, чем это вызвано. Модифицируйте программу таким образом, чтобы она давала правильное значение указанного числа независимо от машины, на которой выполняется.

Упражнение 3.5. Напишите функцию itob(n,s,b), которая переводит целое n в строку s, представляющую число по основанию b. В частности, itob(n, s, 16) помещает в s текст числа n в шестнадцатеричном виде.

Упражнение 3.6. Напишите версию itoa с дополнительным третьим аргументом, задающим минимальную ширину поля. При необходимости преобразованное число должно слева дополняться пробелами.

3.7 Инструкции break и continue

Иногда бывает удобно выйти из цикла не по результату проверки, осуществляемой в начале или в конце цикла, а каким-то другим способом. Такую возможность для циклов for, while и do-while, а также для переключателя switch предоставляет инструкция break. Эта инструкция вызывает немедленный выход из самого внутреннего из объемлющих ее циклов или переключателей.

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

/* trim: удаляет завершающие пробелы, табуляции и новые строки */ int trim(char s[]) < int n; for (n = strlen(s)-1; n >= 0, n--) if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n') break; s[n+1] = '\0'; return n; >

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

Инструкция continue в чем-то похожа на break, но применяется гораздо реже. Она вынуждает ближайший объемлющий ее цикл (for, while или do-while) начать следующий шаг итерации. Для while и do-while это означает немедленный переход к проверке условия, а для for - к приращению шага. Инструкцию continue можно применять только к циклам, но не к switch. Внутри переключателя switch, расположенного в цикле, она вызовет переход к следующей итерации этого цикла.

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

for (i = 0; i < n; i++) < if (a[i] < 0) /* пропуск отрицательных элементов */ continue; . /* обработка положительных элементов */ >

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

3.8 Инструкция goto и метки

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

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

for (. ) for (. ) < . if (disaster) /* если бедствие */ goto error; /* уйти на ошибку */ error: /* обработка ошибки */ ликвидировать беспорядок

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

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

В качестве еще одного примера рассмотрим такую задачу: определить, есть ли в массивах a и b совпадающие элементы. Один из возможных вариантов ее реализации имеет следующий вид:

for (i = 0; i < n; i++) for (j = 0; j < m; j++) if (a[i] == b[i]) goto found; /* нет одинаковых элементов */ . found: /* обнаружено совпадение: a[i] == b[i] */

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

found = 0; for (i = 0; i < n && !found; i++) for (j = 0; j < m && ! found; j++) if (a[i] == b[j]) found = 1; if (found) /* обнаружено совпадение: a[i-1] == b[j-1] */ . else /* нет одинаковых элементов */ .

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

Глава 4. Функции и структура программы

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

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

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

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

4.1 Основные сведения о функциях

Начнем с того, что сконструируем программу, печатающую те строки вводимого текста, в которых содержится некоторый "образец", заданный в виде строки символов. (Эта программа представляет собой частный случай функции grep системы UNIX.) Рассмотрим пример: в результате поиска образца "ould" в строках текста

Ah Love! could you and I with Fate conspire To grasp this sorry Scheme of Things entire, Would not we shatter it to bits -- and then Re-mould it nearer to The Heart's Desire!
Ah Love! could you and I with Fate conspire Would not we shatter it to bits — and then Re-mould it nearer to the Heart's Desire!

Работа по поиску образца четко распадается на три этапа:

while (существует еще строка) if (строка содержит образец) напечатать ее 

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

Конструкция "while (существует еще строка)" реализована в getline (см. главу 1), а фразу "напечатать ее" можно записать с помощью готовой функции printf. Таким образом, нам остается перевести на Си только то, что определяет, входит ли заданный образец в строку.

Чтобы решить эту задачу, мы напишем функцию strindex(s,t), которая указывает место (индекс) в строке s, где начинается строка t, или -1, если s не содержит t. Так как в Си нумерация элементов в массивах начинается с нуля, отрицательное число -1 подходит в качестве признака неудачного поиска. Если далее нам потребуется более сложное отождествление по образцу, мы просто заменим strindex на другую функцию, оставив при этом остальную часть программы без изменений. (Библиотечная функция strstr аналогична функции strindex и отличается от последней только тем, что возвращает не индекс, а указатель.)

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

#include #define MAXLINE 1000 /* максимальный размер вводимой строки */ int getline(char line[], int max); int strindex(char source[], char searchfor[]); char pattern[] ="ould"; /* образец для поиска */ /* найти все строки, содержащие образец */ main() < char line[MAXLINE]; int found = 0; while (getline(line, MAXLINE) >0) if (strindex(line, pattern) >= 0) < printf ("%s", line); found++; >return found; > /* getline: читает строку в s, возвращает длину */ int getline(char s[], int lim) < int c, i; i = 0; while (--lim >0 && (c=getchar()) != EOF && с != '\n') /* I.B.: misprint was here -lim instead of --lim */ s[i++] = c; if (c == '\n') s[i++] = c; s[i] = '\0'; return i; > /* strindex: вычисляет место t в s или выдает -1, если t нет в s */ int strindex (char s[], char t[]) < int i, j, k; for (i = 0; s[i] != '\0'; i++) < for (j = i, k = 0; t[k] != '\0' && s[j] == t[k]; j++, k++) ; if (k >0 && t[k] == '\0') return i; > return –1; >

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

тип-результата имя-функции (объявления аргументов) < объявления и инструкции >

Отдельные части определения могут отсутствовать, как, например, в определении "минимальной" функции

dummy()

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

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

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

return выражение;

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

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

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

Механизмы компиляции и загрузки Си-программ, расположенных в нескольких исходных файлах, в разных системах могут различаться. В системе UNIX, например, эти работы выполняет упомянутая в главе 1 команда cc. Предположим, что три функции нашего последнего примера расположены в трех разных файлах: main.с, getline.c и strindex.c. Тогда команда

cc main.с getline.c strindex.c

скомпилирует указанные файлы, поместив результат компиляции в файлы объектных модулей main.o, getline.o и strindex.o, и затем загрузит их в исполняемый файл a.out. Если обнаружилась ошибка, например в файле main.с, то его можно скомпилировать снова и результат загрузить ранее полученными объектными файлами, выполнив следующую команду:

cc main.с getline.o strindex.o

Команда cc использует стандартные расширения файлов ".с" и ".о", чтобы отличать исходные файлы от объектных.

Упражнение 4.1. Напишите функцию strindex(s, t), которая выдает позицию самого правого вхождения t в s или -1, если вхождения не обнаружено.

4.2 Функции, возвращающие нецелые значения

В предыдущих примерах функции либо вообще не возвращали результирующих значений (void), либо возвращали значения типа int. А как быть, когда результат функции должен иметь другой тип? Многие вычислительные функции, как, например, sqrt, sin и cos, возвращают значения типа double; другие специальные функции могут выдавать значения еще каких-то типов. Чтобы проиллюстрировать, каким образом функция может возвратить нецелое значение, напишем функцию atof(s), которая переводит строку s в соответствующее число с плавающей точкой двойной точности. Функция atof представляет собой расширение функции atoi, две версии которой были рассмотрены в главах 2 и 3. Она имеет дело со знаком (которого может и не быть), с десятичной точкой, а также с целой и дробной частями, одна из которых может отсутствовать. Наша версия не является высококачественной программой преобразования вводимых чисел; такая программа потребовала бы заметно больше памяти. Функция atof входит в стандартную библиотеку программ: ее описание содержится в заголовочном файле .

Прежде всего отметим, что объявлять тип возвращаемого значения должна сама atof, так как этот тип не есть int. Указатель типа задается перед именем функции.

#include /*atof: преобразование строки s в double */ double atof (char s[]) < double val, power; int i, sign; for (i = 0; isspace(s[i]); i++) ; /* игнорирование левых символов-разделителей */ sign = (s[i] == '-') ? –1 : 1; if (s[i] =='+' || s[i] =='-') i++; for (val = 0.0; isdigit (s[i]); i++) val = 10.0 * val + (s[i] - '0'); if (s[i] == '.') i++; for (power = 1.0; isdigit(s[i]; i++) < val = 10.0 * val + (s.[i] - '0'); power *= 10.0; >return sign * val / power; >

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

#include #define MAXLINE 100 /* примитивный калькулятор */ main() < double sum, atof (char[]); char line[MAXLINE]; int getline (char line[], int max); sum = 0; while (getline(line, MAXLINE) >0) printf ("\t%g\n", sum += atof(line)); return 0; >
double sum, atof (char[]);

говорится, что sum - переменная типа double, a atof - функция, которая принимает один аргумент типа char[] и возвращает результат типа double.

Объявление и определение функции atof должны соответствовать друг другу. Если в одном исходном файле сама функция atof и обращение к ней в main имеют разные типы, то это несоответствие будет зафиксировано компилятором как ошибка. Но если функция atof была скомпилирована отдельно (что более вероятно), то несоответствие типов не будет обнаружено, и atof возвратит значение типа double, которое функция main воспримет как int, что приведет к бессмысленному результату.

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

sum += atof(line);

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

double atof();

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

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

/* atoi: преобразование строки s в int с помощью atof */ int atoi (char s[])

Обратите внимание на вид объявления и инструкции return. Значение выражения в

return выражение;

перед тем, как оно будет возвращено в качестве результата, приводится к типу функции. Следовательно, поскольку функция atoi возвращает значение int, результат вычисления atof типа double в инструкции return автоматически преобразуется в тип int. При преобразовании возможна потеря информации, и некоторые компиляторы предупреждают об этом. Оператор приведения явно указывает на необходимость преобразования типа и подавляет любое предупреждающее сообщение.

Упражнение 4.2. Дополните функцию atof таким образом, чтобы она справлялась с числами вида

123.45e-6

в которых после мантиссы может стоять e (или E) с последующим порядком (быть может, со знаком).

4.3 Внешние переменные

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

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

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

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

В связи с приведенными рассуждениями разберем пример. Поставим себе задачу написать программу-калькулятор, понимающую операторы +, -, * и /. Такой калькулятор легче будет написать, если ориентироваться на польскую, а не инфиксную запись выражений. (Обратная польская запись применяется в некоторых карманных калькуляторах и в таких языках, как Forth и Postscript.) В обратной польской записи каждый оператор следует за своими операндами. Выражение в инфиксной записи, скажем

(1 - 2) * (4 + 5)

в польской записи представляется как

1 2 – 4 5 + *

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

Реализовать нашу программу весьма просто. Каждый операнд посылается в стек; если встречается оператор, то из стека берется соответствующее число операндов (в случае бинарных операторов два) и выполняется операция, после чего результат посылается в стек. В нашем примере числа 1 и 2 посылаются в стек, затем замещаются на их разность -1. Далее в стек посылаются числа 4 и 5, которые затем заменяются их суммой (9). Числа -1 и 9 заменяются в стеке их произведением (т. е. -9). Встретив символ новой строки, программа извлекает значение из стека и печатает его.

Таким образом, программа состоит из цикла, обрабатывающего на каждом своем шаге очередной встречаемый оператор или операнд:

while (следующий элемент не конец-файла) if (число) послать его в стек else if (оператор) взять из стека операнды выполнить операцию результат послать в стек else if (новая-строка) взять с вершины стека число и напечатать else ошибка 

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

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

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

#include /* могут быть в любом количестве */ #define /* могут быть в любом количестве */ объявления функций для main main() <. >внешние переменные для push и pop void push (double f) <. >double pop (void) <. >int getop(char s[]) <. > подпрограммы, вызываемые функцией getop 

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

Функция main - это цикл, содержащий большой переключатель switch, передающий управление на ту или иную ветвь в зависимости от типа оператора или операнда. Здесь представлен более типичный случай применения переключателя switch по сравнению с рассмотренным в параграфе 3.4.

#include #include /* для atof() */ #define MAXOP 100 /* макс. размер операнда или оператора */ #define NUMBER '0' /* признак числа */ int getop (char []); void push (double); double pop (void); /* калькулятор с обратной польской записью */ main() < int type; double op2; char s[MAXOP]; while ((type = getop (s)) != EOF) < switch (type) < case NUMBER: push (atof(s)); break; case '+': push (pop() + pop()); break; case '*': push (pop() * pop()); break; case '-': op2 = pop(); push (pop() - op2); break; case '/': pop2 = pop(); if (op2 != 0.0) push (pop() / op2); else printf("ошибка: деление на нуль\n"); break; case '\n': printf("\t%.8g\n", pop()); break; default: printf("ошибка: неизвестная операция %s\n", s); break; >> return 0; >

Так как операторы + и * коммутативны, порядок, в котором операнды берутся из стека, не важен, однако в случае операторов - и /, левый и правый операнды должны различаться. Так, в

push(pop() – pop()); /* НЕПРАВИЛЬНО */

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

#define MAXVAL 100 /* максимальная глубина стека */ int sp = 0; /* следующая свободная позиция в стеке */ double val[MAXVAL]; /* стек */ /* push: положить значение f в стек */ void push(double f) < if (sp < MAXVAL) val[sp++] = f; else printf("ошибка: стек полон, %g не помещается\n", f); >/* pop: взять с вершины стека и выдать в качестве результата */ double pop(void) < if (sp >0) return val[--sp]; else < printf ("ошибка: стек пуст\n"); return 0.0; >>

Переменная считается внешней, если она определена вне функции. Таким образом, стек и индекс стека, которые должны быть доступны и для push, и для pop, определяются вне этих функций. Но main не использует ни стек, ни позицию в стеке, и поэтому их представление может быть скрыто от main.

Займемся реализацией getop - функции, получающей следующий оператор или операнд. Нам предстоит решить довольно простую задачу. Более точно: требуется пропустить пробелы и табуляции; если следующий символ - не цифра и не десятичная точка, то нужно выдать его; в противном случае надо накопить строку цифр с десятичной точкой, если она есть, и выдать число NUMBER в качестве результата.

#include int getch(void); void ungetch(int); /* getop: получает следующий оператор или операнд */ int getop(char s[]) < int i, с; while ((s[0] = с = getch()) == ' ' || с == '\t') ; s[1] = '\0; if (!isdigit(c) && с != '.') return c; /* не число */ i = 0; if (isdigit(c)) /* накапливаем целую часть */ while (isdigit(s[++i] == с = getch())) ; if (с =='.') /* накапливаем дробную часть */ while (isdigit(s[++i] = с = getch())) ; s[i] = '\0'; if (c != EOF) ungetch(c); return NUMBER; >

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

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

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

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

#define BUFSIZE 100 char buf[BUFSIZE]; /* буфер для ungetch */ int bufp = 0; /* след. свободная позиция в буфере */ int getch(void) /* взять (возможно возвращенный) символ */ < return (bufp >0) ? buf[--bufp] : getchar(); > void ungetch(int c) /* вернуть символ на ввод */ < if (bufp >= BUFSIZE) printf("ungetch: слишком много символов\n"); else buf[bufp++] = с; >

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

Упражнение 4.3. Исходя из предложенной нами схемы, дополните программу- калькулятор таким образом, чтобы она "понимала" оператор получения остатка от деления (%) и отрицательные числа.

Упражнение 4.4. Добавьте команды, с помощью которых можно было бы печатать верхний элемент стека (с сохранением его в стеке), дублировать его в стеке, менять местами два верхних элемента стека. Введите команду очистки стека.

Упражнение 4.5. Предусмотрите возможность использования в программе библиотечных функций sin, ехр и pow. См. библиотеку в приложении B (параграф 4).

Упражнение 4.6. Введите команды для работы с переменными (легко обеспечить до 26 переменных, каждая из которых имеет имя, представленное одной буквой латинского алфавита). Добавьте переменную, предназначенную для хранения самого последнего из напечатанных значений.

Упражнение 4.7. Напишите программу ungets(s), возвращающую строку s во входной поток. Должна ли ungets "знать" что-либо о переменных buf и bufp, или ей достаточно пользоваться только функцией ungetch?

Упражнение 4.8. Предположим, что число символов, возвращаемых назад, не превышает 1. Модифицируйте с учетом этого факта функции getch и ungetch.

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

Упражнение 4.10. В основу программы калькулятора можно положить применение функции getline, которая читает целиком строку; при этом отпадает необходимость в getch и ungetch. Напишите программу, реализующую этот подход.

4.4 Области видимости

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

• Как писать объявления, чтобы на протяжении компиляции используемые переменные были должным образом объявлены?

• В каком порядке располагать объявления, чтобы во время загрузки все части программы оказались связаны нужным образом?

• Как организовать объявления, чтобы они имели лишь одну копию?

• Как инициализировать внешние переменные?

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

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

Область действия внешней переменной или функции простирается от точки программы, где она объявлена, до конца файла, подлежащего компиляции. Например, если main, sp, val, push и pop определены в одном файле в указанном порядке, т. е.

main() int sp = 0; double val[MAXVAL]; void push(double f) double pop(void)

то к переменным sp и val можно адресоваться из push и pop просто по их именам; никаких дополнительных объявлений для этого не требуется. Заметим, что в main эти имена не видимы так же, как и сами push и pop.

Однако, если на внешнюю переменную нужно сослаться до того, как она определена, или если она определена в другом файле, то ее объявление должно быть помечено словом extern.

Важно отличать объявление внешней переменной от ее определения. Объявление объявляет свойства переменной (прежде всего ее тип), а определение, кроме того, приводит к выделению для нее памяти. Если строки

int sp; double val[MAXVAL];

расположены вне всех функций, то они определяют внешние переменные sp и val, т. e. отводят для них память, и, кроме того, служат объявлениями для остальной части исходного файла. А вот строки

extern int sp; extern double val[];

объявляют для оставшейся части файла, что sp - переменная типа int, а val - массив типа double (размер которого определен где-то в другом месте); при этом ни переменная, ни массив не создаются, и память им не отводится.

На всю совокупность файлов, из которых состоит исходная программа, для каждой внешней переменной должно быть одно-единственное определение; другие файлы, чтобы получить доступ к внешней переменной, должны иметь в себе объявление extern. (Впрочем, объявление extern можно поместить и в файл, в котором содержится определение.) В определениях массивов необходимо указывать их размеры, что в объявлениях extern не обязательно. Инициализировать внешнюю переменную можно только в определении. Хотя вряд ли стоит организовывать нашу программу таким образом, но мы определим push и pop в одном файле, а val и sp - в другом, где их и инициализируем. При этом для установления связей понадобятся такие определения и объявления:

В файле 1: extern int sp; extern double val[]; void push(double f) <. >double pop(void)

В файле2: int sp = 0; double val[MAXVAL];

Поскольку объявления extern находятся в начале файла1 и вне определений функций, их действие распространяется на все функции, причем одного набора объявлений достаточно для всего файла1. Та же организация extern-объявлений необходима и в случае, когда программа состоит из одного файла, но определения sp и val расположены после их использования.

4.5 Заголовочные файлы

Теперь представим себе, что компоненты программы-калькулятора имеют существенно большие размеры, и зададимся вопросом, как в этом случае распределить их по нескольким файлам. Программу main поместим в файл, который мы назовем main.с; push, pop и их переменные расположим во втором файле, stack.с; a getop - в третьем, getop.c. Наконец, getch и ungetch разместим в четвертом файле getch.с; мы отделили их от остальных функций, поскольку в реальной программе они будут получены из заранее скомпилированной библиотеки.

Существует еще один момент, о котором следует предупредить читателя, - определения и объявления совместно используются несколькими файлами. Мы бы хотели, насколько это возможно, централизовать эти объявления и определения так, чтобы для них существовала только одна копия. Тогда программу в процессе ее развития будет легче и исправлять, и поддерживать в нужном состоянии. Для этого общую информацию расположим в заголовочном файле calc.h, который будем по мере необходимости включать в другие файлы. (Строка #include описывается в параграфе 4.11) В результате получим программу, файловая структура которой показана ниже:

main.с: #include #include #include "calc.h" #define MAXOP 100 main() < . >calc.h: #define NUMBER '0' void push(double); double pop(void); int getop(char[]); int getch(void); void ungetch(int); getop.c: #include #include #include "calc.h" getop () < . >getch.c: #include #define BUFSIZE 100 char buf[BUFSIZE]; intbufp = 0; int getch(void) < . >void ungetch(int) < . >stack.с: #include #include "calc.h" #define MAXVAL 100 int sp = 0; double val[MAXVAL]; void push(double) < . >double pop(void)

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

4.6 Статические переменные

Переменные sp и val в файле stack.с, а также buf и bufp в getch.с находятся в личном пользовании функций этих файлов, и нет смысла открывать к ним доступ кому-либо еще. Указание static, примененное к внешней переменной или функции, ограничивает область видимости соответствующего объекта концом файла. Это способ скрыть имена. Так, переменные buf и bufp должны быть внешними, поскольку их совместно используют функции getch и ungetch, но их следует сделать невидимыми для "пользователей" функций getch и ungetch.

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

static char buf[BUFSIZE]; /* буфер для ungetch */ static int bufp = 0; /* след. свободная позиция в buf */ int getch(void) void ungetch(int с)

то никакая другая программа не будет иметь доступ ни к buf, ни к bufp, и этими именами можно свободно пользоваться в других файлах для совсем иных целей. Точно так же, помещая указание static перед объявлениями переменных sp и val, с которыми работают только push и pop, мы можем скрыть их от остальных функций.

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

Объявление static можно использовать и для внутренних переменных. Как и автоматические переменные, внутренние статические переменные локальны в функциях, но в отличие от автоматических, они не возникают только на период работы функции, а существуют постоянно. Это значит, что внутренние статические переменные обеспечивают постоянное сохранение данных внутри функции.

Упражнение 4.11. Модифицируйте функцию getop так, чтобы отпала необходимость в функции ungetch. Подсказка: используйте внутреннюю статическую переменную.

4.7 Регистровые переменные

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

register int х; register char с;

и т. д. Объявление register может применяться только к автоматическим переменным и к формальным параметрам функции. Для последних это выглядит так:

f(register unsigned m, register long n)

На практике существуют ограничения на регистровые переменные, что связано с возможностями аппаратуры. Располагаться в регистрах может лишь небольшое число переменных каждой функции, причем только определенных типов. Избыточные объявления register ни на что не влияют, так как игнорируются в отношении переменных, которым не хватило регистров или которые нельзя разместить на регистре. Кроме того, применительно к регистровой переменной независимо от того, выделен на самом деле для нее регистр или нет, не определено понятие адреса (см. главу 5). Конкретные ограничения на количество и типы регистровых переменных зависят от машины.

4.8 Блочная структура

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

if (n > 0) < int i; /* описание новой переменной i */ for (i = 0; i

областью видимости переменной i является ветвь if, выполняемая при n>0; и эта переменная никакого отношения к любым i, расположенным вне данного блока, не имеет. Автоматические переменные, объявленные и инициализируемые в блоке, инициализируются каждый раз при входе в блок. Переменные static инициализируются только один раз при первом входе в блок.

Автоматические переменные и формальные параметры также "затеняют" внешние переменные и функции с теми же именами. Например, в

int x; int y; f(double х)

x внутри функции f рассматривается как параметр типа double, в то время как вне f это внешняя переменная типа int. То же самое можно сказать и о переменной y.

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

4.9 Инициализация

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

При отсутствии явной инициализации для внешних и статических переменных гарантируется их обнуление; автоматические и регистровые переменные имеют неопределенные начальные значения ("мусор").

Скалярные переменные можно инициализировать в их определениях, помещая после имени знак = и соответствующее выражение:

int х = 1; char squote = '\''; long day = 1000L * 60L * 60L * 24L; /* день в миллисекундах */

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

int binsearch(int х, int v[], int n)

int low, high, mid; low = 0; high = n - 1;

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

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

int days[] = ;

Если размер массива не указан, то длину массива компилятор вычисляет по числу заданных инициализаторов; в нашем случае их количество равно 12.

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

char pattern[] = "ould";

представляющая собой более короткий эквивалент записи

char pattern[] = ;

В данном случае размер массива равен пяти (четыре обычных символа и завершающий символ '\0').

4.10 Рекурсия

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

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

#include /* printd: печатает n как целое десятичное число */ void printd(int n) < if (n < 0) < putchar('-'); n = -n; >if (n / 10) printd(n / 10); putchar(n % 10 + '0'); >

Когда функция рекурсивно обращается сама к себе, каждое следующее обращение сопровождается получением ею нового полного набора автоматических переменных, независимых от предыдущих наборов. Так, в обращении printd(123) при первом вызове аргумент n = 123, при втором - printd получает аргумент 12, при третьем вызове - значение 1. Функция printd на третьем уровне вызова печатает 1 и возвращается на второй уровень, после чего печатает цифру 2 и возвращается на первый уровень. Здесь она печатает 3 и заканчивает работу.

Следующий хороший пример рекурсии - это быстрая сортировка, предложенная Ч.А.Р. Хоаром в 1962 г. Для заданного массива выбирается один элемент, который разбивает остальные элементы на два подмножества - те, что меньше, и те, что не меньше него. Та же процедура рекурсивно применяется и к двум полученным подмножествам. Если в подмножестве менее двух элементов, то сортировать нечего, и рекурсия завершается.

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

/* qsort: сортирует v[left]. v[right] по возрастанию */ void qsort(int v[], int left, int right) < int i, last; void swap(int v[], int i, int j); if (left >= right) /* ничего не делается, если */ return; /* в массиве менее двух элементов */ swap(v, left, (left + right)/2); /* делящий элемент */ last = left; /* переносится в v[0] */ for(i = left+1; i 

В нашей программе операция перестановки оформлена в виде отдельной функции (swap), поскольку встречается в qsort трижды.

/* swap: поменять местами v[i] и v[j] */ void swap(int v[], int i, int j)

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

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

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

Упражнение 4.13. Напишите рекурсивную версию функции reverse(s), переставляющую элементы строки в ту же строку в обратном порядке.

4.11 Препроцессор языка Си

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

4.11.1 Включение файла

Средство #include позволяет, в частности, легко манипулировать наборами #define и объявлений. Любая строка вида

#include "имя-файла"
#include имя-файла>

заменяется содержимым файла с именем имя-файла. Если имя-файла заключено в двойные кавычки, то, как правило, файл ищется среди исходных файлов программы; если такового не оказалось или имя-файла заключено в угловые скобки < и >, то поиск осуществляется по определенным в реализации правилам. Включаемый файл сам может содержать в себе строки #include.

Часто исходные файлы начинаются с нескольких строк #include, ссылающихся на общие инструкции #define и объявления extern или прототипы нужных библиотечных функций из заголовочных файлов вроде . (Строго говоря, эти включения не обязательно являются файлами; технические детали того, как осуществляется доступ к заголовкам, зависят от конкретной реализации.)

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

4.11.2 Макроподстановка

Определение макроподстановки имеет вид:

#define имя замещающий-текст 

Макроподстановка используется для простейшей замены: во всех местах, где встречается лексема имя, вместо нее будет помещен замещающий-текст. Имена в #define задаются по тем же правилам, что и имена обычных переменных. Замещающий текст может быть произвольным. Обычно замещающий текст завершает строку, в которой расположено слово #define, но в длинных определениях его можно продолжить на следующих строках, поставив в конце каждой продолжаемой строки обратную наклонную черту \. Область видимости имени, определенного в #define, простирается от данного определения до конца файла. В определении макроподстановки могут фигурировать более ранние #define-определения. Подстановка осуществляется только для тех имен, которые расположены вне текстов, заключенных в кавычки. Например, если YES определено с помощью #define, то никакой подстановки в printf("YES") или в YESMAN выполнено не будет.

Любое имя можно определить с произвольным замещающим текстом. Например:

#define forever for( ; ; ) /* бесконечный цикл */

определяет новое слово forever для бесконечного цикла.

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

#define max(A, B) ((A) > (B) ? (A) : (B))

Хотя обращения к max выглядят как обычные обращения к функции, они будут вызывать только текстовую замену. Каждый формальный параметр (в данном случае A и B) будет заменяться соответствующим ему аргументом. Так, строка

x = max(p+q, r+s);

будет заменена на строку

x = ((p+q) > (r+s) ? (p+q) : (r+s));

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

Если вы внимательно проанализируете работу max, то обнаружите некоторые подводные камни. Выражения вычисляются дважды, и если они вызывают побочный эффект (из-за инкрементных операций или функций ввода-вывода), это может привести к нежелательным последствиям. Например,

max(i++, j++) /* НЕВЕРНО */

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

#define square(x) x*x /* НЕВЕРНО */

вызвать square (z+1).

Тем не менее макросредства имеют свои достоинства. Практическим примером их использования является частое применение getchar и putchar из , реализованных с помощью макросов, чтобы из6ежать расходов времени от вызова функции на каждый обрабатываемый символ. Функции в обычно также реализуются с помощью макросов. Действие #define можно отменить с помощью #undef:

#undef getchar int getchar(void)

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

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

#define dprint(expr) printf(#expr " = %g\n", expr)
dprint(x/y);
printf("x/y" " = %g\n", x/y);

а в результате конкатенации двух соседних строк получим

printf("x/y=%g\n", x/y);

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

Оператор ## позволяет в макрорасширениях конкатенировать аргументы. Если в замещающем тексте параметр соседствует с ##, то он заменяется соответствующим ему аргументом, а оператор ## и окружающие его символы-разделители выбрасываются. Например, в макроопределении paste конкатенируются два аргумента

#define paste(front, back) front ## back

так что paste(name, 1) сгенерирует имя name1.

Правила вложенных использований оператора ## не определены; другие подробности, относящиеся к ##, можно найти в приложении A.

Упражнение 4.14. Определите swap(t,x,y) в виде макроса, который осуществляет обмен значениями указанного типа t между аргументами x и y. (Примените блочную структуру.)

4.11.3 Условная компиляция

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

Вычисляется константное целое выражение, заданное в строке #if. Это выражение не должно содержать ни одного оператора sizeof или приведения к типу и ни одной enum-константы. Если оно имеет ненулевое значение, то будут включены все последующие строки вплоть до #endif, или #elif, или #else. (Инструкция препроцессора #elif похожа на else if.) Выражение defined(имя) в #if есть 1, если имя было определено, и 0 в противном случае.

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

#if !defined(HDR) #define HDR /* здесь содержимое hdr.h */ #endif

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

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

#if SYSTEM == SYSV #define HDR "sysv.h" #elif SYSTEM == BSD #define HDR "bsd.h" #elif SYSTEM == MSDOS #define HDR "msdos.h" #else #define HDR "default.h" #endif #include HDR

Инструкции #ifdef и #ifndef специально предназначены для проверки того, определено или нет заданное в них имя. И следовательно, первый пример, приведенный выше для иллюстрации #if, можно записать и в таком виде:

#ifndef HDR #define HDR /* здесь содержимое hdr.h */ #endif

Оставьте свой комментарий !
Автор Комментарий к данной статье apimanager

неплохая статья, все описано достаточно понятно даже для начинающего
2009-07-24 17:56:05
я нечего не поняла 
2010-12-13 19:33:34
Хороший мануал по C
2011-08-16 17:16:38
Какой мануал? Это выдержка глав из книги авторов указанных в начале страницы. (Источник, надо заметить, не последней редакции).
2011-11-01 23:09:29

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *