Справочник от Автор24
Поделись лекцией за скидку на Автор24

Алгоритмические языки и программирование

  • ⌛ 2015 год
  • 👀 589 просмотров
  • 📌 539 загрузок
  • 🏢️ НовГУ
Выбери формат для чтения
Загружаем конспект в формате doc
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Конспект лекции по дисциплине «Алгоритмические языки и программирование» doc
Министерство образования и науки Российской Федерации Государственное образовательное учреждение высшего образования «Новгородский государственный университет имени Ярослава Мудрого» Институт электронных и информационных систем Кафедра информационных технологий и систем Алгоритмические языки и программирование Дисциплина по направлению 010302.62 – Прикладная математика и информатика Конспект лекций Разработал доцент кафедры ИТИС ________________Л.И.Винник _____ ________________2015 г. Принято на заседании кафедры Заведующий кафедрой ______________А.Л. Гавриков _____ ________________2015 г. Содержание Модуль 1 Общие принципы разработки программного обеспечения 1. Введение. Цели и методика изучения дисциплины. Основные сведения о ВМ. Основные концепции ЯП: парадигмы, критерии оценки. Литература 9 Цели и методика изучения дисциплины 9 Основные сведения о ВМ 9 Концепция Фон – Неймановской ВМ 10 Принцип двоичного кодирования 10 Принцип программного управления 10 Принцип однородности памяти 10 Принцип адресности 11 Фон-неймановская архитектура ВМ 11 Основные концепции языков программирования 12 Парадигмы языков программирования 13 Критерии оценки языков программирования 15 Литература 19 2 Программы как промышленные изделия. Критерии качества ПО. Жизненный цикл ПО (ЖЦПО) 19 Понятие жизненного цикла программного обеспечения 19 Модели ЖЦПО 20 Критерии качества ПО 23 Документирование 24 3 ЖЦПО в учебном процессе Алгоритм и программа. Основные управляющие алгоритмические структуры. Способы описания алгоритмов. Стиль программирования 24 Жизненный цикл программного обеспечения в учебном процессе 24 Этап постановки задачи 25 Этап проектирования 27 Этап кодирования алгоритмов 31 Этап тестирования и отладки программ 32 4 Пример разработки программы 33 I Постановка задачи 33 II Проектирование 35 Кодирование 36 5 Программы на ЯВУ: анализ программ; утверждения о программах; корректность программ; способы конструирования и верификации программ; правила вывода для основных структур программирования 43 Вычисление выражений 44 Выполнение оператора присваивания 44 Выполнение условного оператора if 45 Выполнение последовательности операторов 46 Выполнение цикла с условием продолжения while 46 Выполнение вызова подпрограммы call 47 Основы доказательства корректности 47 Определения 48 Предусловия и постусловия в доказательствах корректности 48 Правила вывода (доказательства) 49 Правило вывода Р1 — Усиление предусловия и ослабление постусловия 49 Правило вывода А1 -получение предусловия оператора присваивания 50 Правило вывода А2 - Проверка предусловия оператора присваивания 51 Правило вывода IF1 — Проверка предусловия условного оператора if 52 Правило вывода IF2 - Получение предусловия условного оператора if 53 Правило вывода IF3 — Условный оператор if 53 Правило вывода IF4 - Условный оператор if 54 Правило вывода SI — Последовательность операторов 54 Правило вывода Wl - Цикл с условием продолжения без инициализации 55 Правило вывода W2 — Цикл с условием продолжения с инициализацией 56 Правило вывода DC1 - разделяй и властвуй 57 Правило вывода DC2 - разделяй и властвуй 58 Правило вывода DC3 — разделяй и властвуй 58 Правило вывода DC4 - разделяй и властвуй 58 Правило вывода SP1 — Подпрограмма или сегмент программы 59 Правило вывода SP2 — Подпрограмма или сегмент программы 59 Правило вывода SP3 - Подпрограмма или сегмент программы 59 Применение правил вывода 60 Выбор подходящего правила вывода 60 Следствия для документации к программам 60 Анализ-проверка корректности программы 61 Оператор присваивания 61 Присваивание значения простой переменной 61 Модуль 2. Программирование на языке Си 6. Состав языка: алфавит, идентификаторы, ключевые слова, знаки операций, константы, комментарии 62 Общие синтаксические критерии 62 Алфавит. Используемые символы 63 Идентификаторы 65 Константы 65 Ключевые слова 67 Комментарии 67 Ввод / вывод на консоль 68 Чтение и запись символов 69 Трудности использования getchar() 70 Альтернативы getchar() 70 Чтение и запись строк 71 Форматный ввод / вывод на консоль 72 Функция printf() 73 Функция scanf() 78 Операнды и операции 83 Приведение типов 86 Операции 86 Преобразования при вычислении выражений 88 Операции отрицания и дополнения 88 Операции разадресации и адреса 89 Операция sizeof 89 Мультипликативные операции 90 Аддитивные операции 90 Операции сдвига 91 Поразрядные операции 91 Логические операции 92 Операция последовательного вычисления 92 Условная операция 92 Операции увеличения и уменьшения 93 Простое присваивание 93 Составное присваивание 93 Приоритеты операций и порядок вычислений 94 Побочные эффекты 94 7 Типы данных: Концепция типов данных. Основные типы данных 95 Типы данных и их объявление 95 Категории типов данных 95 Модификация базовых типов 96 Переменные в формате ПЗ 97 Указатели 97 Переменные перечислимого типа 98 Определение объектов и типов 99 Инициализация данных 101 8 Преобразование типов 102 Неявное преобразование типов 103 Преобразование целых типов со знаком 103 Преобразование целых типов без знака 104 Преобразования плавающих типов 104 Преобразование типов указателя 104 Операции языка Си 104 Оператор присваивания 105 Преобразование типов при присваиваниях 105 Множественные присваивания 106 Составное присваивание 106 Арифметические операции 106 Операции увеличения (инкремента) и уменьшения (декремента) 107 Операции сравнения и логические операции 108 Поразрядные операции 109 Условная операция 112 Операция получения адреса (&) и раскрытия ссылки (*) 112 Операция определения размера sizof 114 Оператор последовательного вычисления: оператор "запятая" 114 Оператор доступа к члену структуры (оператор . (точка)) и оператор доступа через указатель -> (оператор стрелка) 115 Оператор [] и () 115 Сводка приоритетов операций 115 Выражения 116 Преобразования при вычислении выражений 116 Преобразования при вызове функции 117 Преобразования при приведении типов 117 9 Типы данных, определяемых пользователем: переименование типов, перечисления, структуры, объединения 117 Переменные перечислимого типа 117 Структуры 118 Объединения 119 Поля битов 120 Переменные с изменяемой структурой 120 Определение объектов и типов 122 Инициализация данных 123 10 Структура программы на Си. Разработка программ линейной структуры 2 124 Структура программы на языке С 124 Библиотека и компановка 125 Раздельная компиляция 126 Компиляция программы на языке С 126 Карта памяти программы на языке С 126 11 Управляющая структура Ветвление. Полное, неполное Ветвление, Выбор. Правила организации и тестирование разветвленных алгоритмов 127 Операторы 127 Пустой оператор 127 Составной оператор (блок) 128 Оператор выражение 128 Операторы безусловного перехода 129 Условные операторы. Правила организации разветвлений 130 12 Основные управляющие алгоритмические структуры Цикл: цикл с параметром (ДЛЯ); цикл с предусловием (ПОКА); цикл с постусловием (ПОВТОРЯТЬ-ДО). Правила организации циклических алгоритмов. Проблемы и методика тестирования циклических алгоритмов 136 Цикл for 137 Варианты цикла for 138 Цикл while 142 Цикл do-while 143 Вложенность операторов цикла 144 Операторы continue и break в операторах цикла 144 13 Разработка циклических алгоритмов при работе с простыми данными: контролируемый ввод; итерации и рекурсия; создание диалоговых программ 145 Правила организации циклических алгоритмов 145 Контролируемый ввод 146 Создание диалоговых программ 146 Пример создания цифрового меню 146 Пример создания циклического меню 147 Циклические алгоритмы 150 Рекурсивные алгоритмы 154 Основные понятия 154 Трассировка рекурсивной функции 156 Виды рекурсии 156 14 Операторы передачи управления 156 Оператор перехода 156 Оператор return 157 Оператор goto 157 Оператор break 157 Функция exit() 158 Оператор continue 159 Модуль 3. Модульное программирование. 15, 16 Функции 160 Область действия функции 161 Аргументы функции 161 Вызовы по значению и по ссылке 162 Вызов по ссылке 162 Вызов функций с помощью массивов 163 Аргументы функции main(): argv и argc 165 Оператор return 168 Возврат из функции 168 Возврат значений 169 Возвращаемые указатели 170 Функция типа void 171 Возвращаемое значение функции main() 171 Рекурсия 172 Прототип функции 173 Устаревшие объявления функций 174 Прототипы устаревших библиотечных функций 175 Объявление списков параметров переменной длины 175 Правило "неявного int" 175 Устаревшие и современные объявления параметров функций 176 Ключевое слово inline 177 Перегрузка имен функций 177 17 Классы памяти 177 18 Рекурсивные алгоритмы: понятие, глубина рекурсии, рекурсивный спуск и подъем, граничное условие. Правила организации рекурсивных алгоритмов 179 Рекурсивные алгоритмы. Основные понятия 179 Фрейм стека рекурсивной функции 179 Глубина рекурсии, конечный шаг, рекурсивный спуск и подъем 180 Трассировка рекурсивной функции 180 Виды рекурсии 181 Свойства рекурсивных задач и решений 181 Правила разработки рекурсивных функций 182 Технология решения простых рекурсивных задач 183 Рекурсивные и итерационные алгоритмы 185 Достоинства и недостатки рекурсивных подпрограмм 185 Демонстрационные примеры 186 Пример вычисления функции Cosin(x) 186 Вычислить сумму элементов линейного массива A[1..N]. 187 Определение количества цифр во вводимом с клавиатуры числе 187 Возведение в степень 187 Алгоритм двоичного поиска. Рекурсия и поисковые задачи 187 19 Массивы: описание, внутреннее представление. Примеры работы с одномерными массивами: инициализация, ввод/вывод, суммирование значений, поиск элемента, слияние массивов, разбиение массивов, сдвиг элементов в массиве, удаление и вставка элементов 188 Массивы и строки 188 Одномерные массивы 188 Создание указателя на массив 189 Передача одномерного массива в функцию 190 Инициализация массивов 192 20 Алгоритмы сортировки массивов: метод выбора, метод вставки, метод быстрой сортировки Хоара, метод Шелла. Поиск в массиве 193 Метод простых вставок 193 Метод прямого выбора 195 Быстрая сортировка (метод Хоара). Рекурсивный алгоритм быстрой сортировки. 195 Быстрая сортировка Хоара.Нерекурсивный алгоритм 197 Метод Шелла 199 Оценки эффективности алгоритмов сортировки 200 Поиск 202 21 Основные алгоритмы работы с многомерными массивами 205 Двухмерные массивы 205 Массивы строк 207 Инициализация безразмерных массивов 208 Массивы переменной длины 209 Приемы использования массивов и строк на примере игры в крестики-нолики 209 Многомерные массивы 211 Индексация указателей 212 22 Указатели: описание, инициализация, операции с указателями, многоуровневые указатели, динамическое выделение памяти 213 Указатели 213 Операции для работы с указателями 214 Указательные выражения 214 Присваивание указателей 214 Преобразование типа указателя 215 Адресная арифметика 216 Указатели и массивы 218 Многоуровневая адресация 219 Инициализация указателей 220 Указатели на функции 222 Функции динамического распределения 224 Динамическое выделение памяти для массивов 225 Указатели с квалификатором restrict 227 Трудности при работе с указателями 227 24 Строки: определение, инициализация, функции для работы со строками. Алгоритмы поиска подстроки в строке. Алгоритм Кнута-Морисса-Пратта 229 Строки 229 Линейный метод поиска подстроки в строке 230 Алгоритм Кнута-Морриса_Пратта. Постановка задачи о точном совпадении 231 Идея сдвига Кнута – Морриса – Пратта 231 Препроцессинг для метода КМП 232 Реализация алгоритма КМП 233 25 Структуры в Си. Массивы структур, вложенные структуры, указатели на структуры 235 Структуры 235 Присваивание структур 237 Массивы структур 238 Пример со списком рассылки 238 Передача членов структур функциям 243 Передача целых структур функциям 243 Указатели на структуры 245 Объявление указателя на структуру 245 Использование указателей на структуры 245 Массивы и структуры внутри структур 247 Объединения 247 Битовые поля 249 Перечисления 251 Важное различие между С и С++ 252 Использование sizof для обеспечения переносимости 253 Средство typedef 254 26 Организация линейных списков: линейный однонаправленный односвязный список 254 Операции со списками при последовательном хранении 255 27 Битовые поля структур и объединения 256 Битовые поля 256 Объединение (union) 257 Перечислимый тип данных 257 28 Потоковый ввод-вывод. Типы потоков, основные функции работы с потоками 257 Файловый ввод / вывод в С и С++ 257 Файловый ввод / вывод в стандартном С и UNIX 257 Потоки и файлы 258 Потоки 258 Файлы 258 Основы файловой системы 259 Указатель файла 259 Открытие файла 260 Закрытие файла 261 Запись символа 261 Чтение символа 261 Использование fopen(), getc(), putc(), и fclose() 262 Использование feof() 263 Ввод / вывод строк: fputs() и fgets() 264 Функция rewind() 264 Функция ferror() 265 Стирание файлов 266 Дозапись потока 267 Функции fread() и fwrite() 267 Использование fread() и fwrite() 267 Пример со списком рассылки 268 Ввод / вывод при прямом доступе: функция fseek() 272 Функции fprinf() и fscanf() 273 Стандартные потоки 274 Связь с консольным вводом / выводом 275 Перенаправление стандартных потоков: функция freopen() 275 29 Препроцессор языка Си: директивы, макросы и предопределенные макросы 276 Директива #define 276 Определение макросов с формальными параметрами 277 Директива #error 278 Директива #include 278 Директивы условной компиляции 279 Директивы #if, #else, #elif и #endif 279 Директивы #ifdef и #ifndef 281 Директива #undef 281 Использование defined 282 Директива #line 282 Директива #pragma 282 Операторы препроцессора # и ## 282 Имена предопределенных макрокоманд 283 Модуль 1 Общие принципы разработки программного обеспечения 1. Введение. Цели и методика изучения дисциплины. Основные сведения о ВМ. Основные концепции ЯП: парадигмы, критерии оценки. Литература Цели и методика изучения дисциплины Основная цель дисциплины - привить студентам, будущим программистам, навыки разработки хорошо продуманных программных продуктов. Конкретный язык программирования при этом рассматривается как инструмент реализации основных алгоритмов и основных функций программного продукта. Курс базируется на изучении одного из процедурно-ориентированных языков. Стандартно изучается ЯПВУ С. В учебном процессе программы разрабатываются под DOS в среде Windows. Основными задачами дисциплины являются: • Привитие навыков постановки задачи. • Привитие навыков проектирования и тестирования программ по принципу нисходящего проектирования с привлечением основных принципов структурного программирования. • Изучение одного из процедурных языков программирования (например, С). В результате изучения дисциплины студент должен: • знать - основные принципы разработки ПО; 1. - основные принципы тестирования ПО; 2. - возможности языка программирования для создания надежных и эффективных программ средней сложности. • уметь - кодировать программы, отлаживать и, при необходимости – модифицировать отдельные компоненты ПО. • иметь - представление о расширенных возможностях языка программирования, о переносимости программ на другие платформы, например, Unix, Linux, Windows. Особое внимание обращается на этапы постановки и проектирования задач, на структуризацию данных и алгоритмов. На примерах простых задач показана технологическая цепочка постановка – проектирование – кодирование – тестирование. Большое внимание уделяется вопросам документирования по отдельным фазам и программы в целом. Регламент изучения дисциплины Дисциплина изучается два семестра В первом семестре лекций 36 ч, лабораторных 36 ч. Во втором семестре лекций 45 ч., лабораторных работ 45 ч. Экзамены в первом и во втором семестрах. Основные сведения о ВМ Вычислительная машина – это совокупность технических средств, служащих для автоматизированной обработки дискретных данных по заданному алгоритму. В основе архитектуры современных ВМ лежит представление алгоритма решения задачи в виде программы последовательных вычислений. Согласно стандарту ISO 2382/1-84, программа для ВМ — это «упорядоченная последовательность команд, подлежащая обработке» ВМ, где определенным образом закодированные команды программы хранятся в памяти, известна под названием вычислительной машины с хранимой в памяти программой. Идея принадлежит создателям вычислителя ENIAC Эккерту, Мочли и фон Нейману. Еще до завершения работ над ENIAC они приступили к новому Проекту — EDVAC, главной особенностью которого стала концепция хранимой в памяти программы, на долгие годы определившая базовые принципы построения последующих поколений вычислительных машин. Относительно авторства существует несколько версий, но поскольку в законченном виде идея впервые была изложена в 1945 году в статье фон Неймана, именно его фамилия фигурирует в обозначении архитектуры подобных машин, составляющих подавляющую часть современного парка ВМ и ВС. Концепция Фон – Неймановской ВМ Сущность фон-неймановской концепции вычислительной машины можно свести к четырем принципам: • двоичного кодирования; • программного управления; • однородности памяти; • адресности. Принцип двоичного кодирования Согласно этому принципу, вся информация, как данные, так и команды, кодируются двоичными цифрами 0 и 1. Каждый тип информации представляется двоичной последовательностью и имеет свой формат. Последовательность битов в формате, имеющая определенный смысл, называется полем. В числовой информации обычно выделяют поле знака и поле значащих разрядов. В формате команды можно выделить два поля: поле кода операции (КОп) и поле адресов (адресную часть - АЧ). Код операции (КОп) Адресная часть (АЧ) Код операции представляет собой указание, какая операция должна быть выполнена, и задается с помощью r-разрядной двоичной комбинации. Вид адресной части и число составляющих ее адресов зависят от типа команды: в командах преобразования данных АЧ содержит адреса объектов обработки (операндов) и результата; в командах изменения порядка вычислений — адрес следующей команды программы; в командах ввода/вывода — номер устройства ввода/ вывода. Адресная часть также представляется двоичной последовательностью, длину которой обозначим через р. Таким образом, команда в вычислительной машине имеет вид (r + р) - разрядной двоичной комбинации. Принцип программного управления Все вычисления, предусмотренные алгоритмом решения задачи, должны быть представлены в виде программы, состоящей из последовательности управляющих слов — команд. Каждая команда предписывает некоторую операцию из набора операций, реализуемых вычислительной машиной. Команды программы хранятся в последовательных ячейках памяти вычислительной машины и выполняются в естественной последовательности, то есть в порядке их положения в программе. При необходимости, с помощью специальных команд, эта последовательность может быть изменена. Решение об изменении порядка выполнения команд программ принимается либо на основании анализа результатов предшествующих вычислений, либо безусловно. Принцип однородности памяти Команды и данные хранятся в одной и той же памяти и внешне в памяти неразличимы. Распознать их можно только по способу использования. Это позволяет проводить над командами те же операции, что и над числами, и, соответственно, открывает ряд возможностей. Так, циклически изменяя адресную часть команды, можно обеспечить обращение к последовательным элементам массива данных. Такой прием носит название модификации команд и с позиций современного программирования не приветствуется. Более полезным является другое следствие принципа однородности, когда команды одной программы могут быть получены результат исполнения другой программы. Эта возможность лежит в основе трансляции — перевода текста программы с языка высокого уровня на язык конкретной ВМ. Концепция вычислительной машины, изложенная в статье фон Неймана, предполагает единую память для хранения команд и данных. Такой подход был принят в вычислительных машинах, создававшихся в Принстонском университете, из-за чего и получил название принстонской архитектуры. Практически одновременно в Гарвардском университете предложили иную модель, в которой ВМ имела отдельную память команд и отдельную память данных. Этот вид архитектуры называют гарвардской архитектурой. Долгие годы преобладающей была и остается принстонская архитектура, хотя она порождает проблемы пропускной способности тракта «процессор-память». В последнее время в связи с широким использованием кэш-памяти разработчики ВМ все чаще обращаются к гарвардской архитектуре. Принцип адресности Структурно основная память состоит из пронумерованных ячеек, причем процессору в произвольный момент доступна любая ячейка. Двоичные коды команд и данных разделяются на единицы информации, называемые словами, и хранятся в ячейках памяти, а для доступа к ним используются номера соответствующих ячеек — адреса. Фон-неймановская архитектура ВМ В статье фон Неймана определены основные устройства ВМ, с помощью которых должны быть реализованы принципы (двоичного кодирования, программного управления, однородности памяти и адресности). Большинство современных ВМ по своей структуре отвечают принципу программного управления. Такая ВМ (рис 1) содержит: • Память • Устройство управления • Арифметико-логическое устройство • Устройство ввода вывода Рис 1 Структура фон-неймановской вычислительной машины Структура фон-неймановской вычислительной машины В любой ВМ имеются средства для ввода программ и данных к ним. Информация поступает из подсоединенных к ЭВМ периферийных устройств (ПУ) ввода. Результаты вычислений выводятся на периферийные устройства вывода. Связь и взаимодействие ВМ и ПУ обеспечивают порты ввода и порты вывода. Термином порт обозначают аппаратуру сопряжения периферийного устройства с ВМ и управления им. Совокупность портов ввода-вывода называют устройством ввода-вывода (УВВ) или модулем ввода/вывода (МВВ). Введенная информация сначала запоминается в основной памяти, а затем переносится во вторичную память для длительного хранения. Чтобы программа могла выполняться, команды и данные должны располагаться в основной памяти (ОП), организованной таким образом, что каждое двоичное слово храниться в отдельной ячейке, идентифицируемой адресом, причем соединение ячейки памяти имеет следующие по порядку адреса. Доступ к любым ячейкам запоминающего устройства (ЗУ) ОП может производиться в произвольной последовательности. Такой вид памяти называется памятью с произвольным доступом. ОП современных ВМ состоит из полупроводниковых оперативных запоминающих устройств (ОЗУ), обеспечивающих как считывание, так и запись информации. Если необходимо, чтобы часть ОП была энергонезависимой, в состав ОП включают постоянные запоминающие устройства, также обеспечивающие произвольный доступ. Хранящаяся в ПЗУ информация может только считываться, но не записываться. Размер ячейки ОП обычно принимается равным 8 двоичным разрядам – байту. Для хранения больших чисел используются 2,4 или 8 байтов, размещаемых в ячейках с последовательными адресами. В этом случае за адрес ячейки принимается адрес его младшего байта (такой метод называется адресацией по младшему байту или методом остроконечников, микропроцессоры Intel, Dec), в противном случае – адресацией по старшему байту или методом «тупоконечников» – мп Motorola, IBM). Для долговременного хранения больших программ и массивов данных в ВМ обычно имеется дополнительная память, известная как вторичная. Вторичная память энергонезависима и чаще всего реализуется на базе магнитных дисков. Информация в ней храниться в виде специально программно поддерживаемых объектов – файлов (ISO: файл –идентифицированная совокупность экземпляров полностью описанного в конкретной программе типа данных, находящихся вне программы во внешней памяти и доступных программе посредством специальных операций). Устройство управления (УУ) - важнейшая часть ЭВМ, организующая выполнение программ путем реализации функции управления и обеспечивающая функционирование ВМ как единой системы. Пересылка информации между любыми элементами ВМ инициируются своим сигналом управления (СУ), то есть управление вычислительным процессом сводится к выдаче нужного набора СУ в нужной временной последовательности. Цепи СУ показаны на рис. Пунктирными линиями. Основной функцией УУ является формирование управляющих сигналов, отвечающих за извлечение команд из памяти в порядке, определяемом программой, и последующее исполнение этих команд. Кроме того, УУ формирует СУ для синхронизации и координации внутренних и внешних устройств ВМ. Арифметико-логическое устройство (АЛУ) обеспечивает арифметическую и логическую обработку входных данных, в результате которой формируется выходная переменная. Функции АЛУ сводятся к простым арифметическим и логическим операциям, а также операциям сдвига. Помимо результата АЛУ формирует признак результата (флаги), характеризующих полученный результат и события, происшедшего в процесс его получения (равенство нулю, знак, четность, перенос, переполнение). Флаги могут анализироваться АЛУ с целью принятия решения о дальнейшей последовательности выполнения команд программы. УУ и АЛУ тесно взаимосвязаны и их обычно рассматривают как единое устройство, известное как центральный процессор или просто процессор (ЦП). Помимо АЛУ ИУУ в ЦП входит также набор регистров общего назначения (РОН), служащих для промежуточного хранения информации в процессе ее обработки. Основные концепции языков программирования Любую систему обозначений и согласованную с ней систему понятий, которую можно использовать для описания алгоритмов и структур данных, в первом приближении можно считать языком программирования. Каждый из языков программирования создавался для определенных целей и имеет свои достоинства и недостатки. Для того чтобы эффективно использовать и реализовывать языки программирования, необходимо хорошо знать фундаментальные понятия, лежащие в основе их построения. Знание концептуальных основ языков программирования с точки зрения использования и реализации базовых языковых конструкций позволит: • более обоснованно выбрать язык программирования для реализации конкретного проекта; • разрабатывать более эффективные алгоритмы; • систематически пополнять набор полезных языковых конструкций; • ускорить изучение новых языков программирования; • использовать полученные знания как методологическую основу для разработки новых языков программирования; • получить базовые знания, необходимые для разработки трансляторов для языков программирования, поддерживающих разные вычислительные модели. Используемые программистами языки программирования отличаются как своим синтаксисом, так и функциональными возможностями. Различия в синтаксисе играют незначительную роль при изучении концептуальных основ языка программирования, в то время как наличие или отсутствие тех или иных функциональных возможностей существенно влияет на реализацию и область применения языка. Парадигмы языков программирования На сегодняшний день имеются четыре основные парадигмы языков программирования, отражающие вычислительные модели, с помощью которых описывается большинство существующих методов программирования: • императивная; • функциональная; • декларативная; • объектно-ориентированная. Императивные языки Императивные (процедурные) языки — это языки программирования, управляемые командами, или операторами языка. Основной концепцией императивного языка является состояние компьютера — множество всех значений всех ячеек (слов) памяти компьютера. Данная модель вытекает из особенностей аппаратной части компьютера стандартной архитектуры, названной "фон-неймановской" в честь одного из ее авторов — американского математика Джона фон Неймана. В таком компьютере данные, подлежащие обработке, и программы хранятся в одной памяти, называемой оперативной. Центральный процессор получает из оперативной памяти очередную команду на входном языке, декодирует ее, выбирает из памяти указанные в качестве операндов входные данные, выполняет команду и возвращает в память результат. Программа на императивном языке представляет собой последовательность команд (операторов), которые выполняются в порядке их написания. Выполнение каждой команды приводит к изменению состояния компьютера. Основными элементами императивных языков программирования, ориентированных на фон-неймановскую архитектуру, являются переменные, моделирующие ячейки памяти компьютера, и операторы присваивания, осуществляющие пересылку данных. Выполнение оператора присваивания может быть представлено как последовательность обращений к ячейкам памяти за операндами выражения из правой части оператора присваивания, передача их процессору, вычисление выражения и возвращение результата вычисления в ячейку памяти, представляющую собой переменную из левой части оператора присваивания. Императивные языки поддерживают, как правило, один или несколько итеративных циклов, различающихся синтаксисом. Итеративные циклы в фон-неймановской архитектуре выполняются быстро за счет того, что команды программы хранятся в соседних ячейках памяти компьютера. Большинство императивных языков включает в себя конструкции, позволяющие программировать рекурсивные алгоритмы, но их реализация на компьютерах с фон-неймановской архитектурой не эффективна, что связано с необходимостью программного моделирования стековой памяти. К императивным языкам относятся такие распространенные языки программирования, как ALGOL-60, BASIC, FORTRAN , PL/1, Ada, Pascal, С, C++, Java. Языки функционального программирования В языках функционального программирования (аппликативных языках) вычисления в основном производятся путем применения функций к заданному набору данных. Разработка программ заключается в создании из простых функций более сложных, которые последовательно применяются к начальным данным до тех пор, пока не получится конечный результат. Типичная программа, написанная на функциональном языке, имеет следующий вид: функция„ ( ... функция2 (функцияN (данные)) ... ). На практике наибольшее распространение получили язык функционального программирования LISP и два его диалекта: язык Common LISP и язык Scheme. Основной структурой данных языка LISP являются связные списки, элементами которых могут быть либо атомы (идентификаторы или числовые константы), либо другие связные списки. При этом список (KLMN) в терминах данных интерпретируется как список из четырех элементов К, L, М, N, а в терминах программ — как функция К с аргументами L, М и N. Несмотря на то, что многие ученые в области компьютерных наук указывают на преимущества языков функционального программирования по сравнению с императивными, языки функционального программирования не получили широкого распространения из-за невысокой эффективности реализации их на компьютерах с фон-неймановской архитектурой. На компьютерах с параллельной архитектурой трансляторы для функциональных языков реализуются более эффективно, однако они еще не конкурентоспособны по сравнению с реализациями императивных языков. Кроме языка LISP, основной областью применения которого являются системы искусственного интеллекта, известны и другие языки функционального программирования: ML (MetaLanguage) , Miranda и Haskell. Язык ML наряду с функциональным программированием поддерживает императивное программирование, но, в отличие от императивных языков, функции в языке ML могут быть полиморфными и передаваться между подпрограммами как параметры. Программирование, как на императивных, так и на функциональных языках является процедурным. Это означает, что программы на этих языках содержат указания, как нужно выполнять вычисления. Декларативные языки Декларативные языки программирования — это языки программирования, в которых операторы представляют собой объявления или высказывания в символьной логике. Типичным примером таких языков являются языки логического программирования (языки, основанные на системе правил). В программах на языках логического программирования соответствующие действия выполняются только при наличии необходимого разрешающего условия. Программа на языке логического программирования схематично выглядит следующим образом: разрешающее условие 1 —> последовательность операторов 1 разрешающее условие 2 —> последовательность операторов 2 —> разрешающее условие n —> последовательность операторов n. В отличие от императивных языков, операторы программы на языке логического программирования выполняются не в том порядке, как они записаны в программе. Порядок выполнения операторов определяется системой реализации правил. Характерной особенностью декларативных языков является их декларативная семантика. Основная концепция декларативной семантики заключается в том, что смысл каждого оператора не зависит от того, как этот оператор используется в программе. Так, смысл заданного высказывания в языке логического программирования можно точно определить по самому оператору. Декларативная семантика намного проще семантики императивных языков, что может рассматриваться как преимущество декларативных языков над императивными. Наиболее распространенным языком логического программирования является язык Prolog, 59]. Основными областями применения языка Prolog являются экспертные системы, системы обработки текстов на естественных языках и системы управления реляционными базами данных. К языкам программирования, основанным на системе правил, можно отнести языки синтаксического разбора (например, YACC — Yet Another Compiler Compiler), в которых синтаксис анализируемой программы рассматривается в качестве разрешающего условия. Объектно-ориентированные языки Концепция объектно-ориентированного программирования складывается из трех ключевых понятий: абстракция данных, наследование и полиморфизм. Абстракция данных позволяет инкапсулировать множество объектов данных (члены класса) и набор абстрактных операций над этими объектами данных (методы класса), ограничивая доступ к данным только через определенные абстрактные операции. Инкапсуляция позволяет изменять реализацию класса без плохо контролируемых последствий для программы в целом. Наследование — это свойство классов создавать из базовых классов производные, которые наследуют свойства базовых классов и могут содержать новые элементы данных и методы. Наследование позволяет создавать иерархии классов и является эффективным средством внесения изменений и дополнений в программы. Полиморфизм означает возможность одной операции или имени функции ссылаться на любое количество определений функций, зависящих от типа данных параметров и результатов. Это свойство объектно-ориентированных языков программирования обеспечивается динамическим связыванием сообщений (вызовов методов) с определениями методов. В основе объектно-ориентированного программирования лежит объектно-ориентированная декомпозиция. Разработка объектно-ориентированных программ заключается в построении иерархии классов, описывающих отношения между объектами, и в определении классов. Вычисления в объектно-ориентированной программе задаются сообщениями, передаваемыми от одного объекта к другому. Объектно-ориентированная парадигма программирования является попыткой объединить лучшие свойства других вычислительных моделей. Наиболее полно объектно-ориентированная концепция реализована в языке Smalltalk. Поддержка объектно-ориентированной парадигмы в настоящее время включена в такие популярные императивные языки программирования, как Ada 95, Java и C++. Критерии оценки языков программирования Каждый из языков программирования, используемых в настоящее время (FORTRAN, Ada, С, Pascal, Java, ML, LISP, Perl, Postscript, Prolog, C++, Smalltalk, Forth, APL, BASIC, HTML, XML), имеет свои преимущества и недостатки, но, тем не менее, все они относительно удачны по сравнению с сотнями других языков, которые были разработаны и реализованы, использовались какое-то время, но так и не нашли широкого применения. Некоторые успехи или неудачи языка могут быть внешними по отношению к нему. Так, например, использование языка Ada в США для разработки приложений в проектах Министерства обороны было регламентировано Правительством. Аналогично часть успеха языка FORTRAN можно отнести к большой поддержке его различными производителями вычислительной техники, которые потратили много усилий на его реализацию и подробное описание. Широкое распространение таких языков программирования, как Lisp и Pascal объясняется их использованием в качестве объектов теоретического изучения студентами, специализирующимися в области языков программирования и методов их реализации. Рассмотрим свойства, которыми должны в той или иной мере обладать языки программирования. Понятность Понятность (удобочитаемость) конструкций языка — это свойство, обеспечивающее легкость восприятия программ человеком. Это свойство языка программирования зависит от целого ряда факторов, начиная с выбора ключевых слов и заканчивая возможностью построения модульных программ. Понятность конструкций языка зависит от выбора такой нотации языка, которая позволяла бы при чтении текста программы легко выделять основные понятия каждой конкретной части программы, не обращаясь к другой документации на программу. Высокая степень понятности конструкций языка полезна с различных точек зрения: • уменьшаются требования к документированию проекта, если текст программы является центральным элементом документации; • понятность конструкций языка позволяет легче понимать программу и, следовательно, быстрее находить ошибки; • высокая степень понятности конструкций языка позволяет легче сопровождать программу. Это особенно справедливо для программ с большим жизненным циклом, когда поддержание обновляемой сопроводительной документации в условиях неизбежного множества последовательных модификаций может оказаться весьма трудоемким делом. Очевидно, что реализация требований понятности конструкций языка во многом зависит от программиста, который должен по возможности лучше структурировать свою программу и так располагать ее текст, чтобы подчеркнуть структуру программы. Вместе с тем важную роль играет синтаксис и структура языка, используемого программистом. Слишком лаконичный синтаксис может оказаться удобным при написании программ, но вместе с тем усложнить их модификацию. Так программы на APL настолько непонятны, что даже авторы, спустя несколько месяцев после завершения работы над программой затрудняются в их интерпретации. Понятный язык программирования характеризуется тем, что конструкции, обозначающие разные понятия, выглядят по-разному, т. е. семантические различия языка отражаются в его синтаксисе. На нижних уровнях программных конструкций язык должен обеспечивать возможность четкой спецификации того, какие объекты данных подвергаются обработке и как они используются. Эта цель достигается выбором идентификаторов и спецификацией типов данных. В язык нельзя вводить такие ограничения, как максимальная длина идентификаторов или только определенные фиксированные типы данных. Алгоритмические структуры должны выражаться в терминах легко понимаемых структур управления, таких как if ... then ... else и т. п. Ключевые слова не следует сводить к аббревиатурам, символы операций должны отображать их смысл (следствием этого является перегрузка операций в языках программирования). На более высоких уровнях программных конструкций язык должен обеспечивать возможность реализации различных абстракций (определять типы данных, структуры данных и операции над ними), а также разбиения программы на модули и управления областями действия имен. Как правило, неизбежной платой за выполнение требования высокой степени понятности конструкций языка программирования является увеличение длины программы. Надежность Под надежностью понимается степень автоматического обнаружения ошибок, которое может быть выполнено транслятором или операционной средой, в которой выполняется программа. Надежный язык позволяет выявлять большинство ошибок во время трансляции программы, а не во время ее выполнения. Это желательно по двум причинам: • чем раньше при разработке программы обнаружена ошибка, тем меньше стоимость самого проекта; • трансляция может быть выполнена на любой машине, воспринимающей входной язык, в то время как тестирование оттранслированной программы должно выполняться на целевой машине либо с использованием программ интерпретации, специально разработанных для тестирования. Существует несколько способов проверки правильности выполнения программой своих функций: • использование формальных методов верификации программ, • проверка путем чтения текста программы, • прогон программы с тестовыми наборами данных. На практике для проверки правильности программ, как правило, используют некоторую комбинацию этих способов. При этом для обнаружения ошибок во время прогона программы необходимо включать в нее дополнительные операторы вывода промежуточных результатов, которые после отладки программы должны быть удалены. Принципиальным средством достижения высокой надежности языка, поддерживаемым на этапе трансляции, является система типизации данных. Предположим, что массив целых чисел а, содержащий 100 элементов, индексируется целой переменной i. Надежный язык должен обеспечить выполнение условия 1 < i < 100 в любом месте программы, где встречается А [ i]. Это можно сделать двумя способами: • включить в программу явную проверку значений индекса перед каждым обращением к элементу массива; • специфицировать область значений при описании переменной i. В этом случае проверка значения переменной i будет выполняться во время присваивания ей значения. Второй способ является более предпочтительным, т. к. во многих случаях законность изменений значения переменной i может быть проверена во время компиляции программы. Одновременно увеличивается удобочитаемость программы, т. к. область значений, принимаемых каждой переменной, устанавливается явно. Безусловно, имеется предел числа ошибок, которые могут быть обнаружены любым транслятором. Например, логические ошибки в программе не могут быть обнаружены автоматически. Однако ошибок такого рода будет возникать меньше, если сам язык программирования поощряет программиста писать ясные, хорошо структурированные программы, предоставляя ему возможность выбора подходящих языковых конструкций. Отсюда следует, что надежность языка программирования связана с его удобочитаемостью. Гибкость Гибкость языка программирования проявляется в том, сколько возможностей он предоставляет программисту для выражения всех операций, которые требуются в программе, не заставляя его прибегать к вставкам ассемблерного кода или различным ухищрениям. Как правило, при разработке языка обеспечивается достаточная гибкость для соответствующей выбранной области применения, но не больше. Требование гибкости языка конфликтует с требованием надежности, поэтому при выборе языка программирования для решения конкретной задачи необходимо понимать, на основании каких требований сделан выбор и на какие компромиссы при этом придется идти. Например, критерий гибкости особенно существенен при решении задач в режиме реального времени, где может потребоваться работа с широким спектром нестандартного периферийного оборудования. В тех же случаях, когда сбой в программе может привести к тяжелым последствиям, например при управлении работой атомной электростанции, на первый план выступает такая характеристика языка, как надежность. Простота Простота языка обеспечивает легкость понимания семантики языковых обструкций и запоминания их синтаксиса. Простой язык предоставляет простой и единообразный набор понятий, которые могут быть использованы в качестве базовых элементов при разработке алгоритма. При том желательно иметь минимальное количество различных понятий с как можно более простыми и систематизированными правилами их комбинирования — язык должен обладать свойством концептуальной целостности. Концептуальная целостность языка включает в себя три взаимосвязанных аспекта: • экономию, • ортогональность • и единообразие понятий. Экономия понятий языка предполагает использование минимального числа понятий. Ортогональность понятий означает, что между ними не должно быть взаимного влияния. В ортогональном языке любые языковые конструкции можно комбинировать по определенным правилам. Например, выражение и условный оператор некоторого языка программирования ортогональны, если любое выражение можно использовать внутри условного оператора. Единообразие понятий требует согласованного, единого подхода к описанию и использованию всех понятий. Для достижения простоты не обязательно избегать сложных языковых конструкций, но следует стараться не накладывать случайных ограничений на их использование. Так, при реализации массивов следует дать возможность программисту объявлять массивы любого типа данных, допускаемого языком. Если же, наоборот, в языке запретить использование массивов некоторого типа, то в результате язык окажется скорее сложным, чем простым, т. к. основные правила языка изучить и запомнить проще, чем связанные с ними ограничения. Простота уменьшает затраты на обучение программистов и вероятность совершения ошибок, возникающих в результате неправильной интерпретации программистом языковых конструкций. Естественно, упрощать язык можно до определенного предела. Язык высокого уровня с неадекватными управляющими операторами и структурами данных вряд ли можно назвать хорошим. Наиболее простыми являются языки функционального программирования, т. к. они основаны на использовании одной конструкции — вызова функции, который может легко комбинироваться с другими вызовами функций. Естественность Язык должен содержать такие структуры данных, управляющие структуры v операции, а также иметь такой синтаксис, которые позволяли бы отражать в программе логические структуры, лежащие в основе реализуемого алгоритма Наличие различных парадигм программирования напрямую связано с необходимостью реализации последовательных, параллельных и логических алгоритмов. Язык, соответствующий определенному классу алгоритмов, может существенно упростить создание программ для конкретной предметной области. Мобильность Язык, независимый от аппаратуры, предоставляет возможность переносить программы с одной платформы на другую с относительной легкостью. Это позволяет распределить высокую стоимость программного обеспечения на ряд платформ. На практике добиться мобильности довольно трудно, особенно в системах реального времени, в которых одна из задач проектирования языка заключается в максимальном использовании преимуществ базового машинного оборудования. Особенно трудно поддаются решению проблемы, связанные с различающимися длинами слов памяти. На мобильность значительно влияет уровень стандартизации языка. Для языков, имеющих стандартное определение, таких как Ada, FORTRAN, С и Pascal, все реализации языка должны основываться на этом стандарте. Стандарт является единственным способом обеспечения единообразия различных реализаций языка. Международные стандарты разрабатываются Организацией Международных Стандартов (ISO — International Standards Organization). Стандарты обычно создаются для популярных, широко используемых языков программирования. Стоимость Суммарная стоимость использования языка программирования складывается из нескольких составляющих. В нее входят: • стоимость обучения языку; • стоимость создания программы; • стоимость трансляции программы; • стоимость выполнения программы; • стоимость сопровождения программы. Стоимость обучения языку определяется степенью сложности языка. Стоимость создания программы зависит от языка и системы программирования, выбранных для реализации конкретного приложения. Наличие в языке программирования развитых структур данных и конструкций позволяет эффективно использовать язык только при условии его надежной, эффективной и хорошо документированной реализации. Для сокращения времени создания программы необходимо иметь систему программирования, которая включает в себя специализированные текстовые редакторы, тестирующие пакеты, средства для поддержки и модификации нескольких версий программы, развитый графический интерфейс и др. Стоимость трансляции программы тесно связана со стоимостью ее выполнения. Совокупность методов, используемых транслятором для уменьшения объема и/или сокращения времени выполнения оттранслированной программы, называется оптимизацией. Чем выше степень оптимизации, тем качественнее получается результирующая программа и сильнее уменьшается время ее выполнения, но при этом возрастает стоимость трансляции. Разрешение конфликта между стоимостью трансляции и стоимостью выполнения программы осуществляется на этапе создания транслятора в результате анализа области его применения. Так для трансляторов, разрабатываемых для учебных целей, оптимизация не требуется, в то время как для промышленных трансляторов, с помощью которых создаются многократно используемые программы, требуется высокая степень оптимизации. Язык программирования, стоимость реализации которого велика, потенциально имеет меньше шансов на широкое распространение. Одной из причин повсеместного распространения языка Java является бесплатное распространение его реализаций сразу после разработки первой версии языка. Стоимость выполнения программы существенна для программного обеспечения систем реального времени. Системы реального времени должны обеспечивать высокую пропускную способность, чтобы не нарушать ограничений, накладываемых управляемым производственным или технологическим процессом или внешним оборудованием. Поскольку необходимо гарантировать определенное время реакции системы, следует избегать языковых конструкций, ведущих к непредсказуемым издержкам времени выполнения программы (например, при сборке мусора в схеме динамического распределения памяти). Для обычных приложений все снижающаяся стоимость машинного оборудования и все возрастающая стоимость разработки программ позволяют считать, что скорость выполнения программ на сегодняшний день не столь критична. В стоимость сопровождения программы входят затраты на исправление дефектов и модификацию программы в связи с обновлением аппаратуры или расширением функциональных возможностей. Стоимость сопровождения программы в первую очередь зависит от удобочитаемости языка, поскольку сопровождение обычно выполняется лицами, не являющимися разработчиками программного обеспечения. Литература 1. Павловская Т.А.С/С++. Программирование на языке высокого уровня. - СПб.:Питер, 2007. -461 с.:ил. 2. С/С++. Структурное программирование: Практикум/ Т.А. Павловская, Ю.А. Щупак. – СПб.:Питер, 2007. -239 с.:ил. 3. Конструирование программ. Системный подход: учеб. пособие/ И.И. Майер, А.Г. Назаров; НовГУ им. Ярослава Мудрого. – Великий Новгород, 2009 – 167 с. 4. Сабуров С.В. Языки программирования C и C++. - М.: Бук-пресс, 2006. - 647 с. - (Справочное руководство пользователя персонального компьютера). 5. Седжвик Роберт. Фундаментальные алгоритмы на С++. Анализ/Структуры данных/Сортировка/Поиск: Пер. с англ./Роберт Седжвик. – К.: Издательство «ДиаСофт», 2001. – 688с.; 6. Программирование на Visual C++/ С.В. Глушаков, А.В. Коваль, С.А. Черепнин; Худож.-оформ. А.С. Юхтман. – М.:ООО «Издательство АСТ»; Харьков: «Фолио», 2003. – 726, [10] с. – (Учебный курс). 7. Пахомов Б.И. С/С++ и MS Visual C++ 2008 для начинающих. – СПб.: БХВ – Петербург, 2009. – 624с.:ил. 2 Программы как промышленные изделия. Критерии качества ПО. Жизненный цикл ПО (ЖЦПО) Понятие жизненного цикла программного обеспечения В основе деятельности по созданию и использованию программного обеспечения (ПО) лежит понятие его жизненного цикла (ЖЦ). Понятие жизненного цикла программного обеспечения появилось, когда программистское сообщество осознало необходимость перехода от кустарных ремесленнических методов разработки программ к более технологичному мануфактурному, а в перспективе и к промышленному, их производству. ЖЦ является моделью создания и использования ПО, отражающей его различные состояния, начиная с момента возникновения необходимости в данном программном изделии и заканчивая моментом его полного выхода из употребления у всех пользователей. Под жизненным циклом программного обеспечения понимают весь период времени существования ПО, начинающийся с момента выработки первоначальной концепции ПО и кончающийся тогда, когда оно морально устаревает. Жизненный цикл традиционно моделируется в виде некоторого числа последовательных этапов (или стадий, фаз). Этапы ассоциируются с определенными видами работ или функций, выполняемых разработчиками в тот или иной момент развития проекта. Этапы характеризуются направленностью выполняемых функций на достижение локальных (для этапа) целей проекта. Необходимость отслеживания целей приводит к понятию контрольных точек — моментов разработки, когда осуществляется подведение промежуточных итогов, осмысление достигнутого и ревизия сделанных ранее предположений. Как и всякая другая, модель жизненного цикла является абстракцией реального процесса, в которой опущены детали, несущественные с точки зрения назначения модели. В настоящее время не выработано общепринятого разбиения жизненного цикла программной системы на этапы. Иногда этап выделяется как отдельный пункт, иногда - входит в качестве составной части в более крупный этап. Могут варьироваться действия, производимые на том или ином этапе. Нет единообразия и в названиях этих этапов. Традиционно выделяются следующие основные этапы ЖЦ ПО: • анализ требований, • проектирование, • кодирование (программирование), • тестирование и отладка, • эксплуатация и сопровождение. Модели ЖЦПО Первоначально термин жизненный цикл (ЖЦ) продукции был введен в середине 60-х годов19 века голландскими специалистами по качеству Дж. Ван Эттингером и Дж. Ситтинтеном. ЖЦ продукции включает три стадии: • Проектирование • Изготовление • Потребление Петля качества В развитии идеи Эттингера и Ситтинтена была разработана концептуальная модель взаимозависимых видов деятельности, влияющих на качество продукции, так называемая петля (спираль) качества (Quality Loop - QL). Петля качества ориентирует на осознание того, что качество формируется на всех стадиях ЖЦ продукции. В дальнейшем петля была стандартизирована, в настоящее время международный стандарт ISO-9004-1-2000 поддерживает двенадцать стадий ЖЦ продукции. 1. Проектирование и разработка. 2. Подготовка производства. 3. Материально-техническое снабжение. 4. Производство. 5. Контроль и испытания. 6. Упаковка т хранение 7. Распределение и реализация. 8. Монтаж и ввод в эксплуатацию. 9. Техническое сопровождение. 10. Послепродажное обслуживание 11. Управление или восстановление после выработки ресурса 12. Маркетинг (поиск и изучение рынка) Модели ЖЦ определяют порядок исполнения этапов в ходе разработки, а также критерии перехода от этапа к этапу. В соответствии с этим наибольшее распространение получили три модели ЖЦ: • каскадная модель (70-80г.г.). Она предполагает переход на следующий этап после полного окончания работ по предыдущему этапу; • каскадно-возвратная (80-85г.г.). Это итерационная модель разработки ПО с циклами обратной связи между этапами., • спиральная модель (86-90г.г.). Каскадная модель ЖЦПО Каскадную модель можно рассматривать в качестве показательного примера того, какими методами можно минимизировать возвраты на предыдущие этапы. Характерные черты каскадной модели: • завершение каждого этапа (они почти те же, что и в классической модели) проверкой полученных результатов, с целью устранить как можно большее количество проблем, связанных с разработкой изделия; • циклическое повторение пройденных этапов (как в классической модели). Мотивация каскадной модели связана с так называемым управлением качеством программного обеспечения. В связи с ней уточняются понятия этапов, некоторые из них структурируются (спецификация требований и реализация). Можно проследить, как в строгой каскадной модели исправляются ошибки ранних этапов. В соответствии с ее схемой в качестве исходных материалов для деятельности на любом этапе, т.е. как задание на разработку, предъявляются результаты предыдущего этапа, прошедшие соответствующую проверку. При проведении работ этапа может быть выяснено, что задание невыполнимо по одной из следующих причин: • оно противоречиво, т.е. содержит несовместимые или невыполнимые требования; • не выработаны критерии для выбора одного из возможных вариантов решения. Обе ситуации квалифицируются как ошибки задания, т.е. как ошибки предыдущего этапа. Для исправления обнаруженных ошибок работы предыдущего этапа возобновляются. В результате ошибки либо ликвидируются, либо констатируется невозможность их непосредственного исправления. В первом случае работы этапа, вызвавшего возврат, возобновляются с откорректированным заданием. Второй случай квалифицируется как ошибка более раннего этапа. Строгая каскадная модель фиксирует два важных момента жизненного цикла: • точное разделение работ, заданий и ответственности разработчиков этапов и тех, кто, проверяя работы, инициирует переход к следующему этапу; • малые циклы между соседними этапами, в результате которых достигается компромиссное задание. Первый момент — это шаг к осознанию фактического разделения труда. В результате появляется возможность постановки задачи создания автоматизированной поддержки функций ПО. Второй момент можно трактовать как совместное выполнение работ соседних этапов, т.е. их перекрытие. Однако в рамках каскадной модели эти обстоятельства отражаются лишь косвенно. Строгая каскадная модель с незначительными модификациями часто рассматривается в реальных технологических шаблонах как основа жизненного цикла разработки программных систем, когда она строится по последовательной схеме. Основным достижением каскадной модели является завершенность стадий. Это дает возможность планирования затрат и сроков. Кроме того, формируется проектная документация, обладающая полнотой и согласованностью. Каскадная модель применима к небольшим программным проектам, с четко поставленными и не изменяемыми требованиями. Реальный процесс может выявить неудачи на любой стадии, что приводит к откату на одну из предыдущих стадий. Модель такого производства ПО – каскадно-возвратная. Преимущество такой модели заключается в том, что межэтапные корректировки обеспечивают меньшую трудоемкость по сравнению с каскадной моделью; однако, время жизни каждого из этапов растягивается на весь период разработки Рисунок 4 – Каскадная модель ЖЦПО Спиральная модель ЖЦПО Более совершенной является спиральная модель, которая отражает объективно существующий подход разработки ПО. Данный вид модели приведен на рисунке 5. Делает упор на начальные этапы ЖЦ: анализ требований, проектирование спецификаций, предварительное и детальное проектирование. На этих этапах проверяется и обосновывается реализуемость технических решений путем создания прототипов. Каждый виток спирали соответствует поэтапной модели создания фрагмента или версии программного изделия, на нем уточняются цели и характеристики проекта, определяется его качество, планируются работы следующего витка спирали. Анализ версии позволяет уточнить цели и характеристики проекта, проанализировать достигаемое качество, перейти к фазе определения требований следующего витка. Таким образом, последовательно конкретизируются детали проекта и, в результате, выбирается обоснованный вариант, который доводится до реализации. Спиральная модель отражает существующие подходы разработки ПО, позволяет быстрее показать пользователю работоспособный продукт и ускорить процесс уточнения и дополнения требований. Преимущества спиральной модели: • накопление и повторное использование программных средств, моделей и прототипов, • ориентация на развитие и модификацию ПО в процессе его проектирования, Недостатками этой модели являются: • трудность в определении момента перехода на следующий этап, трудность планирования затрат и времени завершения работы Про эту модель говорят, что традиционные этапы жизненного цикла разработки никогда не кончаются. Спираль охвата наглядно иллюстрирует смысл этого тезиса, • возможная неполнота, несогласованность, незавершенность документации. Спираль, раскручивающаяся от центра, послужила основой для многочисленных вариаций на тему отражения в модели жизненного цикла итеративного развития проекта. В учебном процессе принята упрощенная каскадная модель. Предполагает четкое временное планирование и разработку документации. Критерии качества ПО Современные программные продукты (ПП), разрабатываемые фирмами разработчиками, отличаются высокой степенью сложности. ПП создаются для решения задач разных классов, могут использоваться как профессионалами, так и людьми невысокой квалификации. Производители и потребители ПП не связаны напрямую между собой. ПП продается на рынке программных продуктов и, т.е. является промышленным коммерческим изделием. Поэтому его разработка должна быть экономически выгодна. Кроме этого, как любое промышленное изделие, оно должно обладать необходимыми характеристиками качества. В соответствии ISO 91261:19914 характеристики качества являются иерархическими критериями. На верхнем уровне выделены шесть показателей качества, они даны без рекомендаций и комментариев к их применению. Кроме этих критериев стандарт рекомендует использовать ряд характеристик качества второго уровня. Это следующие показатели: Функциональность (функциональная пригодность) – набор атрибутов, основанных на существовании некоторого набора функций и их специализированных свойств. Характеризуется пригодностью для применения, точностью, защищенностью, способностью к взаимодействию и согласованностью со стандартами и правилами проектирования; Надежность – набор атрибутов, основанный на способности ПО поддерживать свой уровень исполнения при заданных условиях для заданного периода времени. Характеризуется: уровнем завершенности (отсутствие ошибок), устойчивостью к ошибкам и перезапускаемостью; Применимость – набор атрибутов, основанных на усилии, необходимом для использования и индивидуальной оценке такого использования, заданным или предполагаемым набором пользователей. Описывается: понятностью, обучаемостью и простотой использования; Эффективность – набор атрибутов, основанный на отношении между уровнем выполнения ПО и количеством используемых ресурсов при заданных условиях. Характеризуется ресурсной и временной экономичностью; Сопровождаемость – набор атрибутов, основанный на усилии, необходимом для совершения специфицированных модификаций. Оценивается: удобством для анализа, изменяемостью, стабильностью и тестируемостью; Переносимость (мобильность) – набор атрибутов, основанный на способности ПО быть переносимым из одной среды в другую. Отражается: адаптируемостью, структурированностью, замещаемостью и внедряемостью. Создать ПП высокого качества возможно только тогда, когда работа по качеству начинается с момента возникновения идеи заказа ПП, и продолжается на всех стадиях его создания. Документирование Любая фаза ЖЦПО должна быть документирована. Документация должна быть четкой, непротиворечивой и полной. Необходимо помнить что: • программное обеспечение создается одними людьми, используется другими; • отдельные стадии ЖЦПО реализуются различными коллективами, поэтому результаты работы, полученные на предыдущей фазе, должны корректно передаваться другому коллективу; • возможна текучесть кадров. Нельзя допустить, что бы уход работника привел к срыву проекта; • руководство проекта и заказчик должны иметь возможность контролировать выполнение проекта на отдельных стадиях и в целом. Создание непротиворечивой и полной документации возможно, только при непрерывном процессе ее создания, на каждой фазе, с внесением всех исправлений. В программистской среде прочно укоренился термин спецификаций программы (specify – точно определять; specification – подробное описание). Спецификации – это описатели отдельных стадий ЖЦПО и проекта в целом, т.е. это – полная документация программы. 3 ЖЦПО в учебном процессе Алгоритм и программа. Основные управляющие алгоритмические структуры. Способы описания алгоритмов. Стиль программирования Жизненный цикл программного обеспечения в учебном процессе Процесс разработки программ в рамках изучаемой дисциплины основан на упрощенной каскадной модели ЖЦПО. При этом полный перечень документов учитывает учебный характер дисциплины и, поэтому, минимизирован. Рисунок 6 – Модель ЖЦПО в учебном процессе Этап постановки задачи Этап формирования требований - наиболее ответственная фаза в ЖЦПО. В рамках изучаемой дисциплины данный этап называется Постановкой задачи, а разрабатываемая на данном этапе документация – внешними спецификациями программы. I Постановка задачи 1. Наименование задачи – краткое, емкое наименование, по возможности отражается суть проблемы. 2. Словесное описание – по возможности полное описание задачи на языке предметной области: • описываются структуры входных и выходных данных; определяются ограничения на значения, требования к точности вычислений; • определяются требования к разрабатываемой программе, ее назначение; • обосновывается принципиальная возможность решения поставленной задачи. Вход - описываются входные данные программы. Выход – описываются выходные данные программы. Описание входа и выхода завершаются составлением внешних спецификаций данных, которые рекомендуется представлять в табличной форме (таблица 1) Таблица 1 Объект программы Имя этого объекта в программе Характеристики Как используется в программе Тип данных Диапазоны представления Прост/структура Вход/выход/Константа 3. Внешние спецификации функции. Описываются функции, выполняемые программой. Например: 1. организация ввода исходных данных; 2. вычислительные задачи, получение выходных данных (результатов); 3. вывод результатов; 4. интерфейсные задачи 4. Внешние спецификации интерфейса Интерфейс – это способ осуществления взаимодействия двух систем. Системами в нашем случае являются программа и пользователь. Программа должна быть дружелюбной и предельно понятной пользователю. Обычно к интерфейсу относят функции: • организует как бы вход в систему: заставка. Здесь должна отображаться информация о программе – наименование, разработчики, назначение; • организация ввода: должно быть явно указано, что вводим, единицы измерения, должны быть выведены подсказки о диапазоне возможных значений, отображена информация о реакции программы на неверный ввод данных пользователем; • организация вывода результата (визуализация): в каком формате (целое, вещественное, символ и т.п.) и с какой точностью будет выводится результат. • организация завершающего экрана: выход из программы, выход с подтверждением или повторный сеанс работы с программой. Эргономические требования к разрабатываемым интерфейсам: • однотипность цветовых решений: ◦ избегать ярких цветов; ◦ красный цвет необходимо использовать только для вывода сообщений об ошибках и других критических ситуациях; ◦ цвета объектов и фона должны быть разными, ни в коем случае нельзя использовать оттенки одного и того же цвета; • удобное расположение данных на экране: ◦ основные объекты – в центре экрана, второстепенные – на периферии; ◦ элементы, общие для различных меню, следует размещать на одном месте 5. Внешние данные тестирования Тестирование – процесс выполнения программы с намерением найти ошибки. Многие организации, занимающиеся созданием ПО, до 50% средств, выделенных на разработку программ, тратят на тестирование, что составляет миллиарды долларов по всему миру. Если учесть тот факт, что по определению в ПО имеется ошибка, если оно не выполняет того, что пользователю разумно от него ошибки, то напрашивается вывод: ошибки в ПО являются его внутренним свойством, а наличие ошибок есть функция, как самого ПО, так и ожиданий пользователей. Это значит, что, как бы долго мы ни тестировали программу или доказывали правильность ее спецификаций, мы никогда не сможем найти в ней все ошибки. Необходимо помнить, что надежность ПО есть вероятность его работы без отказов в течение определенного периода времени, рассчитанная с учетом стоимости для пользователя каждого отказа. Отказ – это событие, заключающееся в нарушении работоспособности. Работоспособным называется такое состояние объекта, при котором он способен выполнять заданные функции с параметрами, установленными требованиями технической документации Восстановление – событие, заключающееся в переходе объекта из неработоспособного состояния в работоспособное в результате устранения отказа. Поэтому наша задача научиться разрабатывать надежное ПО с заданными характеристиками качества. Качество тестирования зависит от своевременного применения эффективных технологий тестирования. Аксиомы тестирования: 1. Хорош тот тест, для которого высока вероятность обнаружить ошибку, а не тот, кторый демонстрирует правильную работу программы; 2. Одна из самых сложных проблем тестирования – решить, когда нужно закончить или как выбрать конечное число тестов, которое дает максимальную отдачу (вероятность обнаружить ошибку) для данных затрат; 3. Невозможно тестировать свою собственную программу; 4. Необходимая часть всякого теста – описание ожидаемых выходных данных или результатов. Ожидаемые результаты должны определяться заранее; 5. Готовьте тесты как для правильных, так и неправильных входных данных; В учебном процессе выявляется все, что может привести к некорректному завершению работы. Тестовые данные должны обеспечить проверку всех возможных условий возникновения ошибок: тестирование должно быть целенаправленным и систематизированным. Этапы процесса тестирования: 1. проверка в нормальных условиях; 2. проверка в экстремальных условиях (граничные значения – очень маленькие значения и очень большие); 3. проверка в исключительных ситуациях (значения лежат за областью допустимых значений). 4. должна быть проверена каждая ветвь алгоритма; 5. очередной тестовый прогон должен проконтролировать нечто такое, что еще не было проверено на предыдущих прогонах; 6. количество элементов последовательностей, точность для итерационных вычислений, количество проходов операторов цикла в тестовых примерах должны задаваться из соображения сокращения объема вычислений, но не должны снижать надежности контроля; 7. анализируются ошибки, которые могут возникнуть из-за неправильных действий пользователя: • ошибки при вводе данных (неправильный ввод). Они бывают 3 классов: ◦ данные вне диапазона; ◦ данные в диапазоне, но неправильное (неверное) по конкретному значению; ◦ неправильный числовой формат; • ошибки, возникающие при вычислениях, связанные с делением на ноль. Данный подэтап завершается документально внешними спецификациями тестирования. Рекомендуется данные тестирования предоставить в табличной форме. Таблица 2 Номер теста Назначение теста Значения исходных данных Ожидаемый результат Реакция программы 6. Пример работающей программы. Этап проектирования Этап проектирования является вторым этапом в ЖЦПО. Этот этап – один из наиболее ответственных при создании качественного ПО. На данной стадии постановка задачи должна привести к алгоритмическому наполнению будущего программного продукта. Этап должен быть проведен очень тщательно, с анализом реализуемости внешних спецификаций функций, данных и интерфейса. Плохое проектирование в дальнейшем приводит к созданию программ (написанию кода), содержащих логические ошибки и ошибки неверного использования данных. Такие ошибки труднее всего обнаружить, а это приводит к некорректным и ненадежным программам. Работа, проведенная на этапе проектирования должна завершится внутренними спецификациями программы. II Проектирование 1. Уточнение наименования программы 2. Выбор метода решения задачи Этот раздел должен содержать: ◦ сравнительный анализ методов решения задачи; ◦ выбор наилучшего метода; ◦ выбор и разработка алгоритма, реализующего выбранный метод. При выполнении сравнительного анализа методов решения задачи необходимо пользоваться критериями, однозначно определенными требованиями ПЗ. В тех случаях, когда существующие методы не могут удовлетворить всем требованиям ПЗ, необходимо выполнить синтез методов решения задачи в заданных граничных условиях. Результатом сравнительного анализа должен быть выбор метода, удовлетворяющего требованиям ПЗ. Выбор метода должен сопровождаться анализом и выбором структур входных и выходных данных. В этом же разделе необходимо провести анализ алгоритмов, реализующих выбранный метод решения задачи. При отсутствии алгоритмов, удовлетворительно реализующих выбранный метод, необходимо разработать алгоритмы, провести их сравнительный анализ и выбрать алгоритм, наиболее полно удовлетворяющий критериям ПЗ. 3. Уточненные глобальные данные и пользовательские типы Описываются структуры входных и выходных данных программы, определенные на предыдущем этапе. Обоснование введения пользовательских структур данных; Описание этих структур; имена пользовательских типов данных; Являются дополнением таблицы этапа ПЗ и имеет структуру этой таблицы. 4. Декомпозиция Декомпозиция программы – процесс разбиения решения задачи на подзадачи, с использованием метода нисходящего проектирования. Выделяются функции с описанием входных и выходных значений. Результат декомпозиции приводится в табличной форме (таблица). Таблица 3 Назначение Имя в программе Параметры Тестируемость /не тестируемость Входные: переменная, тип Выходные: переменная, тип Суть нисходящего проектирования – этапы разбиения задачи на подзадачи. На каждом этапе: • выделяются подзадачи; • отбрасываются детали решения подзадачи - считается, что подзадача решаема; • определяются входные и выходные переменные подзадачи. Затем каждая подзадача разбивается на подзадачи до получения подзадач, реализуемых средствами выбранного языка программирования. В языках программирования высокого уровня формальными средствами абстракции являются процедуры и функции. В рамках учебного процесса отправной точкой первого этапа проектирования может быть выделения подзадач, соответствующих функциональному составу этапа Постановки задачи. 5. Алгоритмизация Алгоритмизация – это техника составления алгоритмов и программ для решения задач на компьютере. На данном подэтапе описываются алгоритмы решения подзадач, выявленных на предыдущем этапе. Алгоритм – это система правил, которая сформулирована на языке, понятном исполнителю, определяет процесс перехода от допустимых исходных данных к некоторому результату и обладает свойствами массовости, конечности, определенности, детерминированности. Свойства алгоритмов: • дискретность: алгоритм должен представлять процесс решения задач как последовательное выполнение простых шагов (этапов); • детерминированность: в последовательности шагов алгоритма после каждого шага указывается какой шаг следует выполнять дальше; если же способ получения последующих величин из каких-либо исходных не приводит к результату, то должно быть указано, что следует считать результатом алгоритма. • массовость: алгоритм решения задачи разрабатывается в общем виде и должен быть применим для некоторого класса задач, различающихся лишь исходными данными. В простейшем случае массовость обеспечивает возможность изменения исходных данных в определенных пределах. • конечность – алгоритм должен приводить к решению задачи за конечное число шагов; • определенность - каждое правило алгоритма должно быть четким и однозначным. Благодаря этому свойству выполнение алгоритма носит механический характер и не требует никаких дополнительных указаний или сведений о решаемой задаче. В соответствии с принципами структурного программирования алгоритм любой сложности должен быть описан с помощью трех управляющих структур: следование, ветвление и цикл. Управляющие структуры могут быть реализованными на любом языке программирования. Поэтому структуры описываются на некотором языке, получившем наименование псевдокод. Кроме того, управляющие структуры могут быть описаны при помощи блок – схем алгоритмов. На любом уровне детализации для описания алгоритмов применяются только стандартные управляющие алгоритмические структуры. Алгоритмы описываются без учета языка программирования на псевдокоде или при помощи блок-схем. При описании блок-схем алгоритмов приняты основные символы из ГОСТ 19.701-90, которые приведены в таблице 3.1. Основные алгоритмические нотации приведены в таблице 3.2. При описании алгоритмов используется следующая терминология: • Процесс – одна или более инструкций по преобразованию входных данных в выходные; • Предопределенный (предописанный) процесс – заранее описанная, логически законченная совокупность инструкций, воспринимаемая на определенном шаге как одна инструкция; • Условие (ветвления, выбора, цикла) – логическое выражение, принимающее значение «истина» или «ложь». Таблица 4 Символ Графическое обозначение Символ Графическое обозначение Начало/конец Комментарий Процесс Ввод/вывод Предопределенный процесс Линия связи Условный (альтернативный) блок Символ переноса Таблица 5 Название Псевдокод Блок-схема Алгоритмическая структура СЛЕДОВАНИЕ Следование Начало Оператор 1; Оператор 2 … Оператор n; Конец Разветвляющиеся алгоритмические структуры Ветвление Начало если <ЛВ> то Оператор 1 иначе Оператор 2 все{если} Конец Неполное ветвление Начало если <ЛВ> то Оператор 1; все{если} Конец Выбор Начало выбор <пер. выбора Q> при Q1: Оператор 1; … при Qn: Оператор n; иначе Оператор n+1; все{выбор} Конец Циклические алгоритмические структуры Цикл Пока (цикл с предусловием) Начало инициализация переменных; пока <ЛВ> нц Оператор (тело цикла} кц Конец Цикл с параметром (переменная х принимает значения из [х1,х2] с шагом х3) Начало для х от х1 до х2 с шагом х3 нц Оператор {тело цикла}; кц Конец Цикл Повторять - До (с постусловием) Начало повторять нц Оператор {тело цикла}; кц до <ЛВ> Конец Этап кодирования алгоритмов Фазой реализации принято называть фазу кодирования спецификаций, разработанных при проектировании. Для создания кода программы используется язык программирования C. Требования к исходному тексту программы: I. при выборе идентификаторов использовать: • для переменных - осмысленные имена; • для функций - составные имена: первая часть имени должна определять действие, вторая – над чем производится действие. II. при оформлении исходного текста программы использовать: 1. комментарии: • сведения об авторе, дате создания и последней модификации программы; • наименование и/или назначение программы; • список и назначение основных переменных; • список функций, их назначение; • наиболее сложные фрагменты кода; 2. пропуски строк. Их используют для деления программы на логически самостоятельные части: • пропуском одной строки может быть обособлена группа логически связанных операторов; • пропуском двух и более строк – более обособленные логические фрагменты кода. 3. пробелы. Пробелы служат для улучшения читабельности программы и используются: • между элементами списка данных; • символами арифметических операций; • скобками и т.п.; 4. отступы. Используются в началах строк для выявления структуры программы. Отступы целесообразны: • при описании структур данных; • в составных операторах: операторах цикла, условных операторах и т.п.; • для указания обособленности фрагмента и выделения структуры. При описании операторов следует избегать размещения нескольких операторов в одной строке, так как снижает читабельность кода и усложняет процесс отладки. Этап тестирования и отладки программ Тестирование – процесс выполнения программы с целью обнаружения ошибок. На данном этапе выполняется прогон программы на тестовых наборах данных, разработанных на этапе постановки задачи. Возникающие в программе ошибки, как правило, классифицируются следующим образом: • синтаксические – тип ошибок, связанный с нарушением синтаксиса языка программирования. Данный тип ошибок обнаруживает компилятор; • времени выполнения – тип ошибок, связанный с невозможностью исполняющей системы выполнить какое либо действие. Данный тип ошибок обнаруживает исполняющая система; • семантические – тип ошибок, связанный с несоответствием ожидаемого результата с полученным. Данный тип ошибок системой не обнаруживается. Ошибки семантики наиболее сложны в плане их выявления. Для их обнаружения используется механизм отладки. Отладка представляет собой набор процедур и действий, начинающихся с выявления самого факта ошибки и заканчивающихся установлением точного места и характера этой ошибки. Отладка программ В Turbo C встроен специальный компонент – отладчик, использование которого приводит к реализации следующих возможностей: 1. пошаговое выполнение программы; 2. контроль значений переменных; 3. использование точек прерывания. Для пошагового выполнения программы служат функциональные клавиши и . Разница режимов пошагового выполнения, задаваемого от состоит в том, что для второго режима функция рассматривается как неделимый оператор и вход в соответствующий ей блок не производится. Контроль над значениями переменных осуществляется через помещение идентификаторов контролируемых переменных в специальное окно «Watch». Точки прерывания могут устанавливаться двумя способами: с помощью комбинации + позиция, в которой находится курсор, становится точкой прерывания, или с помощью осуществляется выполнение программы до строки, в которой находится курсор. Необходимо обратить внимание на параметры настройки, необходимые для работы отладчика: 1) Options/Compiler/Debug information – ‘ON’ 2) Options/Compiler/Local symbols – ‘ON’ 3) Options/Debugger/Debugging Standalone – ‘ON’ 4) Options/Debugger/Debugging Integrated – ‘ON’ 4 Пример разработки программы I Постановка задачи Словесное описание задачи Программа разрабатывается для того, чтобы по введенным пользователем данным: координатам точки приземления по осям ОХ, ОУ и радиусу площадки приземления (в метрах), определить попадет ли спортсмен по прыжкам с парашюта в сборную или нет. Результатом работы программы является сообщение вида «Парашютист (не) попал в сборную». Вход: координаты точки приземления, радиус площадки; Выход: Сообщение. Таблица 1 Объект программы Имя в программе Тип Диапазон Пр/структура Вход/Выход/Const Координаты точки приземления koord_x, koord_y Действит. [-20..20] простая вход Радиус площадки radius Целое без знака [1..10] простая вход Результат result строка Длина в 60 символов структура выход Функциональный состав Организация ввода данных (с контролем диапазона и нецифрового ввода данных); Обработка данных; Организация вывода результата; Интерфейсные задачи; Многоразовое выполнение программы. Спецификации интерфейса Организация заставки Цвет текста – белый, цвет фона – темно-синий, цвет рамки - белый Программа Парашютист Совместная разработка Гр. 1091 Esc – выход, ENTER - далее Организация ввода данных Введите Координаты точки приземления [-20.0..20.0] по оси ОХ __ по оси ОУ __ Радиус площадки приземления [1..10]__ ENTER - далее 3.3. Вывод сообщения об ошибке Введите Координаты точки приземления [-20.0..20.0] по оси ОХ __ по оси ОУ __ Радиус площадки приземления [1..10] Ошибка ввода данных! ENTER - далее 3.4. Организация вывода данных Введены Координаты точки приземления по оси ОХ _*.** по оси ОУ _*.** Радиус площадки приземления * Спортсмен (не) попал в сборную Esc – повтор ввода, ENTER - далее 3.5 Интерфейс завершающего экрана Программа Парашютист Завершила свою работу Нажмите любую клавишу Внешние данные тестирования Входные данные Выход Реакция программы koord_x=’asdf’ koord_y=’12.12.12’ radius=’12.3’ сообщение об ошибке, повтор ввода данных Программа работает нормально koord_x=-21 koord_y=20.1 radius=0 сообщение об ошибке, повтор ввода данных Программа работает нормально koord_x=0.0000000000001 koord_y=0.0000000000001 radius=1 сообщение об ошибке, повтор ввода данных Программа работает нормально koord_x=0.0001 koord_y=0.0001 radius=1 Спортсмен попал в сборную Программа работает нормально koord_x=20.0 koord_y=20.0 radius=10 Спортсмен не попал в сборную Программа работает нормально koord_x=9.9999999999999 koord_y=9.9999999999999 radius=10 Спортсмен попал в сборную Программа работает нормально koord_x=3.0 koord_y=4.0 radius=5 Спортсмен попал в сборную Программа работает нормально koord_x=-2 koord_y=-3 radius=2 Спортсмен не попал в сборную Программа работает нормально koord_x=-3 koord_y=-4 radius=5 Спортсмен попал в сборную Программа работает нормально Пример работающей программы При запуске программы появляется окно заставки (интерфейс 3.1), где пользователю предлагается нажать клавишу Esc для выхода или Enter для продолжения работы программы. Если пользователь нажал клавишу Esc, то появляется завершающий экран (интерфейс 3.5). Если пользователь нажал клавишу Enter, то появляется окно ввода данных (интерфейс 3.2). Курсор устанавливается в место ввода данных, ожидается ввод данных пользователем. Если пользователь ввел некорректные данные (нецифровой ввод данных, вне диапазона), то появляется сообщение об ошибке (интерфейс 3.3). После нажатия клавиши Enter место ввода очищается, ожидается повторный ввод данных. Если данные введены верно, то после нажатия клавиши Enter появляется окно вывода результатов (интерфейс 3.4), где пользователю предлагается повторить ввод данных (клавиша Esc) или продолжить работу с программой (клавиша Enter). Если пользователь нажал клавишу Esc, то появляется окно ввода данных (интерфейс 3.2), работа с программой начинается заново. Если же пользователь нажал клавишу Enter, то появляется интерфейс завершающего экрана (интерфейс 3.5). После нажатия любой клавиши происходит выход из программы. II Проектирование Уточненное наименование программы Parashutist1_0; Уточненные данные программы и пользовательские типы Декомпозиция Назначение подпрограммы Имя в программе Вход : тип Выход: тип Тест/ не тест Примечание Рисование рамки Frame x1,y1,x2,y2:int - Не тест Формирование главного окна MainWindow st1,st2,st3,st4, st5, st6: char[] - Не тест Формирование строки статуса Status St:char[] Не тест Организация заставки ScreenSaver - Ch:char Не тест Организация ввода 1 переменной веществ. типа с контролем диапазона и нецифрового ввода InpFloat x, y:char; {координаты ввода} min, max : float; {границы диапазона} xx:float Тест Организация ввода 1 переменной целого типа с контролем диапазона и нецифрового ввода InpInt x, y:char; {координаты ввода} min, max : int; {границы диапазона} xx:int Тест Организация ввода данных InpData - koord_x, koord_y: float; radius : int Тест Обработка данных Processing koord_x, koord_y: float; radius : int Result : char[] Тест Организация вывода результата OutResult koord_x, koord_y: float; radius : int Result : char[] Ch:char тест Организация завершающего экрана EndScreen - - Не тест Кодирование Алгоритм Frame; Вход: x1,y1,x2,y2:int Выход: - Локальные переменные: i:int Начало Для i от х1 до х2 Делать Нц Установить курсор (i,y1); Вывод(‘-‘); Установить курсор (i,y2); Вывод(‘-‘); Кц Для i от у1 до у2 Делать Нц Установить курсор (х1,i); Вывод(‘|‘); Установить курсор (x2,i); Вывод(‘|‘); Кц Установить курсор (х1,y1); Вывод(‘┌‘); Установить курсор (x2,y1); Вывод(‘┐‘); Установить курсор (х1,y2); Вывод(‘└‘); Установить курсор (x2,y2); Вывод(‘┘‘); Конец Алгоритм MainWindow; Вход: st1, st2, st3, st4, st5, st6:char[] Выход: - Локальные переменные: Начало Очистить экран; Frame(3,3,77,20); Установить курсор(((80-длина_строки(st1)/2), 6); Вывод(st1); Установить курсор(((80-длина_строки(st2)/2), 8); Вывод(st2); Установить курсор(((80-длина_строки(st3)/2), 10); Вывод(st3); Установить курсор(((80-длина_строки(st4)/2), 12); Вывод(st4); Установить курсор(((80-длина_строки(st5)/2), 14); Вывод(st5); Установить курсор(((80-длина_строки(st6)/2), 16); Вывод(st6); Конец Алгоритм Status; Вход: st : shar[] Выход: - Локальные переменные: Начало Frame(3,22,77,24); Установить курсор(((80-длина_строки(st)/2), 23); Вывод(st); Конец Алгоритм ScreenSaver; Вход: Выход: ch:char Локальные переменные: Начало MainWindow(‘Программа’,’Парашютист’,’’,’Совместная разработка’,’’,’гр. 1091’); Status(‘Esc – выход, ENTER - далее’) Повторять Нц Считать код клавиши (ch); Кц До (ch=#13) или (ch=#27); /* Enter Esc Конец Алгоритм InpFloat; Вход: x,y:int; min,max:float; Выход: xx:float Локальные переменные: st:char[]; code:int; Начало Повторять Нц MainWindow(‘Введите’,’Координаты точки приземления [-20..20] ’,’По оси ОХ’,’По оси ОУ’,’Радиус площадки приземления [1..10]’,’’); Status(‘ENTER – далее‘); Установить курсор (x, y); Ввод строки (st) ; Преобразуем st в хх с кодом ошибки code; Если (сode=0) или (ххmax) То начало Status(‘Ошибка ввода. ENTER - далее’); Повторять Нц Считать код клавиши в ch; Кц До (ch=#13); Все {если} Кц До (code=0) и (xx>=min) и (xx<=max); Конец Алгоритм InpInt; Вход: x,y:int; min,max:Int; Выход: xx:int Локальные переменные: st:char[]; code:int; ch:char; Начало Повторять Нц MainWindow(‘Введите’,’Координаты точки приземления [-20..20] ’,’По оси ОХ’,’По оси ОУ’,’Радиус площадки приземления [1..10]’,’’); Status(‘ENTER – далее‘); Установить курсор (x, y); Ввод строки (st) ; Преобразуем st в хх с кодом ошибки code; Если (сode=0) или (ххmax) То начало Status(‘Ошибка ввода. ENTER - далее’); Повторять Нц Считать код клавиши в ch; Кц До (ch=#13); Все {если} Кц До (code=0) и (xx>=min) и (xx<=max); Конец Алгоритм InpData; Вход: - Выход: koord_x, koord_y : float; Radius : int Локальные переменные: Начало InpFloat(60,10,-20,20,koord_x); InpFloat(60,12,-20,20,koord_y); InpInt(60,14,1,10,radius); Конец Алгоритм Processing; Вход: koord_x, koord_y : float; Radius : int Выход: result:char[]; Локальные переменные: Начало Если ((koord_x*koord_x+koord_y*koord_y)<=radius*radius) То Result=”Парашютист попал в сборную” Иначе Result=”Парашютист не попал в сборную” Все{если} Конец Алгоритм OutResult; Вход: koord_x, koord_y : float; Radius : int result:char[]; Выход: ch:char Локальные переменные: Начало MainWindow(‘Введены’,’Координаты точки приземления [-20..20] ’,’По оси ОХ’,’По оси ОУ’,’Радиус площадки приземления [1..10]’,’’); Status(‘Esc – повтор ввода, ENTER – далее‘); Установить курсор(60, 10); Вывод(koord_x); Установить курсор(60, 12); Вывод(koord_y); Установить курсор(60, 14); Вывод(radius); Установить курсор(60, 16); Вывод(result); Повторять Нц Считать код клавиши ch; Кц До (сh=#13) или (ch=#27); Конец Алгоритм EndScreen; Вход:- Выход: - Локальные переменные: - Начало MainWindow(‘Программа’,’,Парашютист’,’,’завершила свою работу’,’’); Status(‘Нажмите любую клавишу‘); Ожидать нажатия любой клавиши; Конец Алгоритм программы; Вход: Выход: - Локальные переменные: сh:char Начало ScreenSaver(ch); Если ch=#13 То Начало Повторять Нц InpData(koord_x,koord_y,radius); Processing(koord_x,koord_y,radius, result); OutResult(koord_x,koord_y,radius, result,ch); Кц До (сh=#13); Конец Все {если} EndScreen; Конец Код программы на ЯВУ С. #include #include #include #include #include #define MAX_KOORD 20.0 #define MAX_RADIUS 10 #define ENTER 13 #define ESC 27 float Koord_x=0.0; float Koord_y=0.0; int Radius=0.0; char *Result; char Ch; void Frame(int x1, int y1, int x2, int y2); void MainWindow(char *text_string1,char *text_string2,char *text_string3,char *text_string4,char *text_string5,char *text_string6); void Status(char *text_string,int color_error); char ScreenSaver(void); float InpFloat(int x, int y, float Min_Value, float Max_Value); int InpInt(int x, int y, int Min_Value, int Max_Value,float koord_x,float koord_y); char *Processing(float koord_x, float koord_y, int radius); void InpData(float *koord_x,float *koord_y, int *radius); char OutResult(float koord_x, float koord_y, int radius, char *result); void EndScreen(void); void main() { clrscr(); Ch=ScreenSaver(); if (Ch==ENTER) { do { InpData(&Koord_x,&Koord_y,&Radius); Result = Processing(Koord_x, Koord_y, Radius); Ch=OutResult(Koord_x, Koord_y, Radius,Result); } while (Ch!=ENTER); } EndScreen(); } void Frame(int x1, int y1, int x2, int y2) { int i; textbackground(BLUE); textcolor(WHITE); for (i=x1+1;i<=x2-1; i++) { gotoxy(i,y1); putch(205); gotoxy(i,y2); putch(205); } for (i=y1;i<=y2; i++) { gotoxy(x1,i); putch(186); gotoxy(x2,i); putch(186); } gotoxy(x1,y1); putch(201); gotoxy(x2,y1); putch(187); gotoxy(x1,y2); putch(200); gotoxy(x2,y2); putch(188); } void MainWindow(char *text_string1,char *text_string2,char *text_string3,char *text_string4,char *text_string5,char *text_string6) { clrscr(); Frame(3,3,77,20); gotoxy(((80-strlen(text_string1))/2),6); cprintf(text_string1); gotoxy(((80-strlen(text_string2))/2),8); cprintf(text_string2); gotoxy(((80-strlen(text_string3))/2),10); cprintf(text_string3); gotoxy(((80-strlen(text_string4))/2),12); cprintf(text_string4); gotoxy(((80-strlen(text_string5))/2),14); cprintf(text_string5); gotoxy(((80-strlen(text_string6))/2),16); cprintf(text_string6); } void Status(char *text_string,int color_error) { Frame(3,22,77,24); gotoxy(((80-strlen(text_string))/2),23); if (color_error==1) { textcolor(RED); cprintf(text_string); textcolor(WHITE); } else cprintf(text_string) ; } char ScreenSaver(void) { char ch; clrscr(); MainWindow(" Программа ",""," Парашютист ",""," Совместная разработка "," гр. 1091 "); Status(" Esc - выход, ENTER - далее ",0); do { ch=getch(); } while ((ch!=ESC) && (ch!=ENTER)); return ch; } float InpFloat(int x, int y, float Min_Value, float Max_Value) { float Tmp; char *Str, *endptr; char ch; do { clrscr(); textbackground(LIGHTBLUE); textcolor(BLACK); MainWindow("Введите","Координаты точки приземления [-20.0..20.0]","по оси OX ","по оси ОY ","Радиус площадки приземления [1..10] ",""); Status("ENTER - далее",0); gotoxy(x,y); cscanf("%s",Str); Tmp=strtod(Str,&endptr); /* Tmp=atof(Str);*/ if ((TmpMax_Value) || (*endptr != NULL)) { Status("Ошибка ввода данных. ESC - повтор ввода",1); do { ch=getch(); } while (ch!=ESC); } } while (!((Min_Value<=Tmp) && (Tmp<=Max_Value) && (*endptr ==NULL))); return Tmp; } int InpInt(int x, int y, int Min_Value, int Max_Value,float koord_x,float koord_y) { int Tmp; char *Str, *endptr; char ch; do { clrscr(); MainWindow("Введите","Координаты точки приземления [-20.0..20.0]","по оси OX ","по оси ОY ","Радиус площадки приземления [1..10] ",""); Status("ENTER - далее",0); gotoxy(60,10); cprintf("%5.2f",koord_x); gotoxy(60,12); cprintf("%5.2f",koord_y); gotoxy(x,y); cscanf("%s",Str); Tmp=strtol(Str,&endptr,0); /*Tmp=atof(Str);*/ if ((TmpMax_Value) || (*endptr != NULL)) { Status("Ошибка ввода данных ! BackSpace - далее",1); do { ch=getch(); } while (ch!=32); } } while (!((Min_Value<=Tmp) && (Tmp<=Max_Value) && (*endptr ==NULL))); return Tmp; } void InpData(float *koord_x,float *koord_y, int *radius) { char ch; *koord_x=InpFloat(60,10,-MAX_KOORD,MAX_KOORD); *koord_y=InpFloat(60,12,-MAX_KOORD,MAX_KOORD); *radius=InpInt(60,14,1,MAX_RADIUS,*koord_x,*koord_y); do { ch=getch(); } while (ch!=ENTER); } char *Processing(float koord_x, float koord_y, int radius) { char *Res; if(float(radius)>=sqrt(pow(koord_x,2)+pow(koord_y,2))) Res="Спортсмен попал в сборную"; else Res="Спортсмен не попал в сборную"; return Res; } char OutResult(float koord_x, float koord_y, int radius, char *result) { char ch; clrscr(); MainWindow("Введены","Координаты точки приземления [-1000.0..1000.0]","по оси ОХ","по оси ОУ","Радиус площадки [0.1..10.0]",""); gotoxy(60,10); cprintf("%5.2f",koord_x); gotoxy(60,12); cprintf("%5.2f",koord_y); gotoxy(60,14); cprintf("%d",radius); gotoxy(30,16); cprintf("%s",result); Status("Esc - повтор ввода данных, Enter - далее",0); do { ch=getch(); } while ((ch!=ESC) && (ch!=ENTER)); return ch; } void EndScreen(void) { clrscr(); Frame(2,2,78,20); gotoxy(32,10); MainWindow(" Программа ",""," Парашютист ",""," Завершила свою работу ",""); Status(" Нажмите любую клавишу ",0); getch(); } Модуль 2. Программирование на языке Си 6. Состав языка: алфавит, идентификаторы, ключевые слова, знаки операций, константы, комментарии Общие синтаксические критерии Синтаксис, определяемый как система языковых категорий, относящихся к соединениям слов и строению предложений, в ЯП описывает последовательность символов, которая составляет синтаксически правильную программу. Синтаксис предоставляет важную информацию, необходимую как для понимания программы, так и для ее трансляции в объектную программу. Семантика ЯП – совокупность правил, определяющих смысл как языковых конструкций, так и программ в целом. Основным назначением синтаксиса ЯП является обозначение системы обозначений для обмена информацией между программистом и процессором языка программирования. Синтаксические элементы языка: 1. набор символов; 2. идентификаторы; 3. символы операций; 4. ключевые и зарезервированные слова. Ключевое слово - это идентификатор, используемый в качестве фиксированной части какого-либо оператора. Ключевое слово является зарезервированным, если синтаксис запрещает его использование в качестве идентификатора, определяемого программистом. Использование зарезервированных слов облегчает синтаксический анализ во время трансляции; 5. необязательные слова – слова, которые вставляются в операторы для удобства чтения (COBOL: оператор GO TO, GO –обязательное слово, а TO – необязательное); 6. комментарии; 7. пробелы. В С пробелы не являются значащими символами, за исключением случаев использования их в строковых литералах; 8. разделители и скобки; Разделитель – это синтаксический элемент, функция которого заключается в обозначении начала и конца некоторой синтаксической конструкции. Например, оператора или выражения. Скобки – это парные ограничители; 9. свободный и фиксированный форматы. Синтаксис называется синтаксисом со свободным форматом записи операторов, если он допускает запись операторов программы в произвольном месте строки ввода безотносительно к его положению в строке или разрывам между строками; 10. выражения. В императивных ЯП выражения формируют основные операции, позволяющие изменять состояние компьютера при выполнении каждого оператора; 11. операторы. Алфавит. Используемые символы Множество символов используемых в языке СИ можно разделить на пять групп. 1. Символы, используемые для образования ключевых слов и идентификаторов. В эту группу входят прописные и строчные буквы английского алфавита, а также символ подчеркивания. Одинаковые прописные и строчные буквы считаются различными символами, так как имеют различные коды. Прописные буквы латинского алфавита A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Строчные буквы латинского алфавита a b c d e f g h i j k l m n o p q r s t u v w x y z Символ подчеркивания (нижнее тире) _ 2. Группа прописных и строчных букв русского алфавита и арабские цифры. Прописные буквы русского алфавита А Б В Г Д Е Ж З И К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ы Ь Э Ю Я Строчные буквы русского алфавита а б в г д е ж з и к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я Арабские цифры 0 1 2 3 4 5 6 7 8 9 3. Знаки нумерации и специальные символы. Эти символы используются с одной стороны для организации процесса вычислений, а с другой - для передачи компилятору определенного набора инструкций. Символ Наименование Символ Наименование , запятая ) круглая скобка правая . точка ( круглая скобка левая ; точка с запятой } фигурная скобка правая : двоеточие { фигурная скобка левая ? вопросительный знак < меньше ' апостроф > больше ! восклицательный знак [ квадратная скобка | вертикальная черта ] квадратная скобка / дробная черта # номер \ обратная черта % процент ~ тильда & амперсанд * звездочка ^ логическое не + плюс = равно - мину " кавычки 4. Управляющие и разделительные символы. К этой группе символов относятся: пробел, символы табуляции, перевода строки, возврата каретки, новая страница и новая строка. Эти символы отделяют друг от друга объекты, определяемые пользователем, к которым относятся константы и идентификаторы. Последовательность разделительных символов рассматривается компилятором как один символ (последовательность пробелов). 5. Кроме выделенных групп символов в языке СИ широко используются так называемые, управляющие последовательности, т.е. специальные символьные комбинации, используемые в функциях ввода и вывода информации. Управляющая последовательность строится на основе использования обратной дробной черты (\) (обязательный первый символ) и комбинацией латинских букв и цифр. Управляющая последовательность Наименование Шеснадцатеричная замена \a Звонок 007 \b Возврат на шаг 008 \t Горизонтальная табуляция 009 \n Переход на новую строку 00A \v Вертикальная табуляция 00B \r Возврат каретки 00C \f Перевод формата 00D \" Кавычки 022 \' Апостроф 027 \0 Ноль-символ 000 \\ Обратная дробная черта 05C \ddd Символ набора кодов ПЭВМ в восьмеричном представлении   \xddd Символ набора кодов ПЭВМ в шестнадцатеричном представлении   Последовательности вида \ddd и \xddd (здесь d обозначает цифру) позволяет представить символ из набора кодов ПЭВМ как последовательность восьмеричных или шестнадцатеричных цифр соответственно. Например символ возврата каретки может быть представлен различными способами: \r - общая управляющая последовательность, \015 - восьмеричная управляющая последовательность, \x00D - шестнадцатеричная управляющая последовательность. В строковых константах всегда обязательно задавать все три цифры в управляющей последовательности. Например, отдельную управляющую последовательность \n (переход на новую строку) можно представить как \010 или \xA, но в строковых константах необходимо задавать все три цифры, в противном случае символ или символы, следующие за управляющей последовательностью, будут рассматриваться как ее недостающая часть. Например: "ABCDE\x009FGH" данная строковая команда будет напечатана с использованием определенных функций языка СИ, как два слова ABCDE FGH, разделенные 8-ю пробелами, в этом случае если указать неполную управляющую строку"ABCDE\x09FGH",то на печати появится ABCDE=|=GH, так как компилятор воспримет последовательность \x09F как символ "=+=". Если обратная дробная черта предшествует символу не являющемуся управляющей последовательностью (т.е. не включенному в табл.4) и не являющемуся цифрой, то эта черта игнорируется, а сам символ представляется как литеральный. Например: символ \h представляется символом h в строковой или символьной константе. Кроме определения управляющей последовательности, символ обратной дробной черты (\) используется также как символ продолжения. Если за (\) следует (\n), то оба символа игнорируются, а следующая строка является продолжением предыдущей. Это свойство может быть использовано для записи длинных строк. Идентификаторы Идентификатором называется последовательность цифр и букв, а также специальных символов, при условии, что первой стоит буква или специальный символ. Для образования идентификаторов могут быть использованы строчные или прописные буквы латинского алфавита. В качестве специального символа может использоваться символ подчеркивание (_). Два идентификатора для образования которых используются совпадающие строчные и прописные буквы, считаются различными. Например: abc, ABC, A128B, a128b . Важной особенностью является то, что компилятор допускает любое количество символов в идентификаторе, хотя значимыми являются первые 31 символ. Идентификатор создается на этапе объявления переменной, функции, структуры и т.п. после этого его можно использовать в последующих операторах разрабатываемой программы. При этом: Во первых, идентификатор не должен совпадать с ключевыми словами, с зарезервированными словами и именами функций библиотеки компилятора языка СИ. Во вторых, следует обратить особое внимание на использование символа (_) подчеркивание в качестве первого символа идентификатора, поскольку идентификаторы построенные таким образом, что, с одной стороны, могут совпадать с именами системных функций и (или) переменных, а с другой стороны, при использовании таких идентификаторов программы могут оказаться непереносимыми, т.е. их нельзя использовать на компьютерах других типов. В третьих, на идентификаторы используемые для определения внешних переменных, должны быть наложены ограничения, формируемые используемым редактором связей. Константы Константами называются перечисление величин в программе. В языке СИ разделяют четыре типа констант: целые константы, константы с плавающей запятой, символьные константы и строковыми литералы. Целая константа: это десятичное, восьмеричное или шестнадцатеричное число, которое представляет целую величину в одной из следующих форм: десятичной, восьмеричной или шестнадцатеричной. Десятичная константа состоит из одной или нескольких десятичных цифр, причем первая цифра не должна быть нулем (в противном случае число будет воспринято как восьмеричное). Восьмеричная константа состоит из обязательного нуля и одной или нескольких восьмеричных цифр (среди цифр должны отсутствовать восьмерка и девятка, так как эти цифры не входят в восьмеричную систему счисления). Шестнадцатеричная константа начинается с обязательной последовательности 0х или 0Х и содержит одну или несколько шестнадцатеричных цифр (цифры представляющие собой набор цифр шестнадцатеричной системы счисления: 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F) Примеры целых констант: Десятичная константа Восьмеричная константа Шестнадцатеричная константа 16 020 0x10 127 0117 0x2B 240 0360 0XF0 Если требуется сформировать отрицательную целую константу, то используют знак "-" перед записью константы (который будет называться унарным минусом). Например: -0x2A, -088, -16 . Каждой целой константе присваивается тип, определяющий преобразования, которые должны быть выполнены, если константа используется в выражениях. Тип константы определяется следующим образом: - десятичные константы рассматриваются как величины со знаком, и им присваивается тип int (целая) или long (длинная целая) в соответствии со значением константы. Если константа меньше 32768, то ей присваивается тип int в противном случае long. - восьмеричным и шестнадцатеричным константам присваивается тип int, unsigned int (беззнаковая целая), long или unsigned long в зависимости от значения константы. Диапазон шестнадцатеричных констант Диапазон восьмеричных констант Тип 0x0 - 0x7FFF 0 - 077777 int 0X8000 - 0XFFFF 0100000 - 0177777 unsigned int 0X10000 - 0X7FFFFFFF 0200000 - 017777777777 long 0X80000000 - 0XFFFFFFFF 020000000000 - 037777777777 unsigned long Для того чтобы любую целую константу определить типом long, достаточно в конце константы поставить букву "l" или "L". Пример: 5l, 6l, 128L, 0105L, OX2A11L. Константа с плавающей точкой - десятичное число, представленное в виде действительной величины с десятичной точкой или экспонентой. Формат имеет вид: [ цифры ].[ цифры ] [ Е|e [+|-] цифры ] . Число с плавающей точкой состоит из целой и дробные части и (или) экспоненты. Константы с плавающей точкой представляют положительные величины удвоенной точности (имеют тип double). Для определения отрицательной величины необходимо сформировать константное выражение, состоящее из знака минуса и положительной константы. Примеры: 115.75, 1.5Е-2, -0.025, .075, -0.85Е2 Символьная константа - представляется символом заключенном в апострофы. Управляющая последовательность рассматривается как одиночный символ, допустимо ее использовать в символьных константах. Значением символьной константы является числовой код символа. Примеры: ' '- пробел , 'Q'- буква Q , '\n' - символ новой строки , '\\' - обратная дробная черта , '\v' - вертикальная табуляция . Символьные константы имеют тип int и при преобразовании типов дополняются знаком. Строковая константа (литерал) - последовательность символов (включая строковые и прописные буквы русского и латинского а также цифры) заключенные в кавычки (") . Например: "Школа N 35", "город Тамбов", "YZPT КОД". Все управляющие символы, кавычка ("), обратная дробная черта (\) и символ новой строки в строковом литерале и в символьной константе представляются соответствующими управляющими последовательностями. Каждая управляющая последовательность представляется как один символ. Например, при печати литерала "Школа \n N 35" его часть "Школа" будет напечатана на одной строке, а вторая часть "N 35" на следующей строке. Символы строкового литерала сохраняются в области оперативной памяти. В конец каждого строкового литерала компилятором добавляется нулевой символ, представляемый управляющей последовательностью \0. Строковый литерал имеет тип char[] . Это означает, что строка рассматривается как массив символов. Число элементов массива равно числу символов в строке плюс 1, так как нулевой символ (символ конца строки) также является элементом массива. Все строковые литералы рассматриваются компилятором как различные объекты. Строковые литералы могут располагаться на нескольких строках. Такие литералы формируются на основе использования обратной дробной черты и клавиши ввод. Обратная черта с символом новой строки игнорируется компилятором, что приводит к тому, что следующая строка является продолжением предыдущей. Например: "строка неопределенной \n длины" полностью идентична литералу "строка неопределенной длинны" . Для сцепления строковых литералов можно использовать символ (или символы) пробела. Если в программе встречаются два или более строковых литерала, разделенные только пробелами, то они будут рассматриваться как одна символьная строка. Этот принцип можно использовать для формирования строковых литералов занимающих более одной строки. Ключевые слова Ключевое слово - это идентификатор, используемый в качестве фиксированной части какого-либо оператора. Ключевое слово является зарезервированным, если синтаксис запрещает его использование в качестве идентификатора, определяемого программистом. Использование зарезервированных слов облегчает синтаксический анализ во время трансляции. Ключевые слова - это зарезервированные идентификаторы, которые наделены определенным смыслом. Их можно использовать только в соответствии со значением известным компилятору языка СИ. Список ключевых слов auto extern register union break else return unsigned case enum short void char float signed volatile continue for sizeof while double if struct default int switch do long tupedef Кроме того в рассматриваемой версии реализации языка СИ, зарезервированными словами являются: _asm, fortran, near, far, cdecl, huge, paskal, interrupt . Ключевые слова far, huge, near позволяют определить размеры указателей на области памяти. Ключевые слова _asm, cdelc, fortran, pascal служат для организации связи с функциями, написанными на других языках, а также для использования команд языка ассемблера непосредственно в теле разрабатываемой программы на языке СИ. Ключевые слова не могут быть использованы в качестве пользовательских идентификаторов. Комментарии Комментарий - это набор символов, который компилятор игнорирует. На этот набор символов, однако, накладываются следующее ограничение: внутри набора символов, который представляет комментарий, не может быть специальных символов определяющих начало и конец комментариев, соответственно (/* и */). Комментарии могут заменить как одну строку, так и несколько. Например: /* комментарии к программе */ /* начало алгоритма */ или /* комментарии можно записать в следующем виде, однако надо быть осторожным, чтобы внутри последовательности, которая игнорируется компилятором, не попались операторы программы, которые также будут игнорироваться */ Неправильное определение комментариев. /* комментарии к алгоритму /* решение краевой задачи */ */ или /* комментарии к алгоритму решения */ краевой задачи */ В стандарте С89 определены комментарии только одного вида; такой комментарий начинается с символов /* и заканчивается символами */. Между звездочкой и слешем не должно быть никаких пробелов. Любой текст, расположенный между начальными и конечными символами комментария, компилятором игнорируется. Например, следующая программа только выведет на экран Привет: #include int main(void) { printf("Привет"); /* printf("всем"); */ return 0; } Комментарий такого вида называется многострочным комментарием (multiline comment), потому что его текст может располагаться в нескольких строках. Например, /* это многострочный комментарий */ Комментарии могут находиться в любом месте программы, за исключением середины ключевого слова или идентификатора. Приведенный ниже комментарий правильный: x = 10+ /* прибавлять числа */5; а комментарий swi/* такое работать не будет */tch(c) { ... не является допустимым, потому что комментарий не может разрывать ключевое слово. Впрочем, комментарии обычно не следует размещать и в середине выражений, потому что так труднее разобраться и с выражениями, и с самими комментариями. Многострочные комментарии не могут быть вложенными. То есть в одном комментарии не может находиться другой. Например, при компиляции следующего фрагмента кода будет обнаружена ошибка: /* Это внешний комментарий x = y/a; /* а это внутренний комментарий, обнаружив который, компилятор выдаст сообщение об ошибке */ */ Однострочные комментарии. В С99 (да и в C++) поддерживается два вида комментариев. Первым из них является /* */, или многострочный комментарий, о котором только что говорилось. А вторым — однострочный комментарий. Такой комментарий начинается с символов // и заканчивается в конце строки. Например: // это однострочный комментарий Однострочные комментарии особенно полезны тогда, когда нужны краткие, не более чем в одну строку пояснения. Хотя версия С89 такие комментарии официально не поддерживает, зато их признает большинство компиляторов С. Однострочный комментарий может находиться внутри многострочного комментария. Например, следующий комментарий является вполне допустимым: /* это // проверка вложенных комментариев */ Комментарии должны находиться там, где требуется объяснить работу кода. Например, в начале всех функций, за исключением самых очевидных, должен быть комментарий, который сообщает, что именно делает функция, как она вызывается и что возвращает. Лекция 6_1 Ввод и вывод информации в С Ввод / вывод на консоль В языке С не определено никаких ключевых слов, с помощью которых можно выполнять ввод/вывод. Вместо них используются библиотечные функции. Заголовочным файлом для функций ввода/вывода является . Имеются как консольные, так и файловые функции ввода/вывода. Ввод/вывод в файл и файловый ввод/вывод синонимы. Рассмотрим функции ввода/вывода на консоль, которые определяются стандартом языка С. В стандарте языка С не определены никакие функции, предназначенные для выполнения различных операций управления экраном (например, позиционирования курсора) или вывода на него графики потому, что эти операции на разных ВМ очень сильно отличаются. В стандартном С не определены никакие функции, которые выполняют операции вывода в обычном или диалоговом окне, создаваемом в среде Windows. Функции ввода/вывода на консоль выполняют всего лишь телетайпный вывод. Однако в библиотеках большинства компиляторов имеются функции графики и управления экраном, предназначенные для той среды, в которой как раз и должны выполняться программы. И, конечно же, на языке С можно писать Windows-программы. Просто в С не определены функции, которые выполняли бы эти задачи напрямую. Консольными функциями ввода /вывода называются те, которые выполняют ввод с клавиатуры и вывод на экран. В действительности же эти функции работают со стандартным потоком ввода и стандартным потоком вывода (устройствами). Более того, стандартный ввод и стандартный вывод могут быть перенаправлены на другие устройства. Таким образом, "консольные функции" не обязательно должны работать только с консолью Стандартный ввод — логический файл для ввода данных, связываемый с физическим файлом или стандартным выводом другой программы при запуске. По умолчанию стандартный ввод в пакетном режиме связывается со входным потоком, а в диалоговом режиме — с терминалом. Стандартный вывод — логический файл вывода данных, связываемый с физическим файлом или стандартным вводом другой программы при запуске. По умолчанию стандартный вывод в пакетном режиме связывается с выходным потоком, а в диалоговом режиме — с терминалом. Чтение и запись символов Самыми простыми из консольных функций ввода/вывода являются getchar(), которая читает символ с клавиатуры, и putchar(), которая отображает символ на экране. Первая из этих функций ожидает, пока не будет нажата клавиша, а затем возвращает значение этой клавиши. Кроме того, при нажатии клавиши на клавиатуре на экране дисплея автоматически отображается соответствующий символ. Вторая же функция, putchar(), отображает символ на экране в текущей позиции курсора. Прототипы функций getchar() и putchar(): int getchar(void); int putchar(int c); Как видно из прототипа, считается, что функция getchar() возвращает целый результат. Однако возвращаемое значение можно присвоить переменной типа char, что обычно и делается, так как символ содержится в младшем байте (старший байт при этом обычно обнулен). В случае ошибки getchar() возвращает EOF. (Макрос EOF определяется в и часто равен -1.) Несмотря на то, что функция putchar()объявлена как принимающая целый параметр, она обычно вызывается с символьным аргументом. На самом деле из ее аргумента на экран выводится только младший байт. Функция putchar() возвращает записанный символ или, в случае ошибки, EOF. В следующей программе продемонстрировано применение getchar() и putchar(). В этой программе с клавиатуры вводятся символы, а затем они отображаются в другом регистре. То есть символы, вводимые в верхнем регистре, выводятся на нижнем, а вводимые в нижнем — выводятся в верхнем. Чтобы остановить программу, необходимо ввести точку. #include #include int main(void) { char ch; printf("Введите какой-нибудь текст. для завершения работы введите точку).\n"); do { ch = getchar(); if (islower(ch)) ch = toupper(ch); else ch = tolower(ch); putchar(ch); } while (ch != '.'); return 0; } (Эта программа не работает с кириллическими символами.) Трудности использования getchar() Использование getchar() может быть связано с определенными трудностями. Во многих библиотеках компиляторов эта функция реализуется таким образом, что она заполняет буфер ввода до тех пор, пока не будет нажата клавиша . Это называется построчно буферизованным вводом. Чтобы функция getchar() возвратила какой-либо символ, необходимо нажать клавишу . Кроме того, эта функция при каждом ее вызове вводит только по одному символу. Поэтому сохранение в буфере целой строки может привести к тому, что в очереди на ввод останутся ждать один или несколько символов, а в интерактивной среде это раздражает достаточно сильно. Хотя getchar() и можно использовать в качестве интерактивной функции, но это делается редко. Альтернативы getchar() Так как getchar(), имеющаяся в библиотеке компилятора, может оказаться неподходящей в интерактивной среде, то для чтения символов с клавиатуры может потребоваться другая функция. В стандарте языка С не определяется никаких функций, которые гарантировали бы интерактивный ввод, но их определения имеются буквально в библиотеках всех компиляторов С. У двух из самых распространенных альтернативных функций getch() и getche() имеются следующие прототипы: int getch(void); int getche(void); В библиотеках большинства компиляторов прототипы таких функций находятся в заголовочном файле . В библиотеках некоторых компиляторов имена этих функций начинаются со знака подчеркивания (_). Например, в Visual C++ компании Microsoft они называются _getch() и _getche(). Функция getch() ожидает нажатия клавиши, после которого она немедленно возвращает значение. Причем, символ, введенный с клавиатуры, на экране не отображается. Имеется еще и функция getche(), которая хоть и такая же, как getch(), но символ на экране отображает. И если в интерактивной программе необходимо прочитать символ с клавиатуры, то часто вместо getchar() применяется getche() или getch(). Текст предыдущей программы, в которой getchar() заменена функцией getch(): #include #include #include int main(void) { char ch; printf("Введите какой-нибудь текст (для завершения работы введите точку).\n"); do { ch = getch(); if(islower(ch)) ch = toupper(ch); else ch = tolower(ch); putchar(ch); } while (ch != '.'); return 0; } Когда выполняется эта версия программы, при каждом нажатии клавиши соответствующий символ сразу передается программе и выводится в другом регистре. А ввод в строках не буферизируется. Чтение и запись строк Среди функций ввода/вывода на консоль есть и более сложные, но и более мощные: это функции gets() и puts(), которые позволяют считывать и отображать строки символов. Прототипы функций char *gets(char *cmp); int puts(const char *cmp); Функция gets() читает строку символов, введенную с клавиатуры, и записывает ее в память по адресу, на который указывает ее аргумент. Символы можно вводить с клавиатуры до тех пор, пока не будет введен символ возврата каретки. Он не станет частью строки, а вместо него в ее конец будет помещен символ конца строки ('0'), после чего произойдет возврат из функции gets(). На самом деле вернуть символ возврата каретки с помощью этой функции нельзя (а с помощью getchar() — как раз можно). Перед тем как нажимать , можно исправлять неправильно введенные символы, пользуясь для этого клавишей возврата каретки на одну позицию (клавишей backspace). Вот прототип для gets(): char *gets(char *cmp); Здесь cmp — это указатель на массив символов, в который записываются символы, вводимые пользователем, gets() также возвращает cmp. Следующая программа читает строку в массив str и выводит ее длину: #include #include int main(void) { char str[80]; gets(str); printf("Длина в символах равна %d", strlen(str)); return 0; } Необходимо очень осторожно использовать gets(), потому что эта функция не проверяет границы массива, в который записываются введенные символы. Таким образом, может случиться, что пользователь введет больше символов, чем помещается в этом массиве. Ее альтернативой, позволяющей предотвратить переполнение массива, будет функция fgets(). Функция puts() отображает на экране свой строковый аргумент, после чего курсор переходит на новую строку. Вот прототип этой функции: int puts(const char *cmp); puts() признает те же самые управляющие последовательности, что и printf(), например, \t в качестве символа табуляции. Вызов функции puts() требует намного меньше ресурсов, чем вызов printf(). Это объясняется тем, что puts() может только выводить строку символов, но не может выводить числа или делать преобразования формата. В результате эта функция занимает меньше места и выполняется быстрее, чем printf(). Поэтому тогда, когда не нужны преобразования формата, часто используется функция puts(). Функция puts() в случае успешного завершения возвращает неотрицательное значение, а в случае ошибки — EOF. Однако при записи на консоль обычно предполагают, что ошибки не будет, поэтому значение, возвращаемое puts(), проверяется редко. Следующий оператор выводит фразу Привет: puts("Привет"); В таблице перечислены основные функции консольного ввода/вывода. Таблица Основные функции ввода/вывода Функция Ее действия getchar() Читает символ с клавиатуры; обычно ожидает возврат каретки getche() Читает символ, при этом он отображается на экране; не ожидает возврата каретки; в стандарте С не определена, но распространена достаточно широко getch() Читает символ, но не отображает его на экране; не ожидает возврата каретки; в стандарте С не определена, но распространена достаточно широко putchar() Отображает символ на экране gets() Читает строку с клавиатуры puts() Отображает строку на экране Пример. Простой компьютеризованный словарь. Показано применение нескольких основных функций консольного ввода/вывода. Эта программа предлагает пользователю ввести слово, а затем проверяет, совпадает ли оно с каким-либо из тех слов, что находятся в ее базе данных. Если оно там есть, то программа выводит значение слова. !!! Внимание на использование косвенной адресации в этой программе: массив dic — это массив указателей на строки. Список должен завершаться двумя нулями!!!. /* Простой словарь. */ #include #include #include /* список слов и их значений */ char *dic[][40] = { "атлас", "Том географических и/или топографических карт.", "автомобиль", "Моторизоравонное средство передвижения.", "телефон", "Средство связи.", "самолет", "Летающая машина.", "", "" /* нули, завершающие список */ }; int main(void) { char word[80], ch; char **p; do { puts("\nВведите слово: "); scanf("%s", word); p = (char **)dic; /* поиск слова в словаре и вывод его значения */ do { if(!strcmp(*p, word)) { puts("Значение:"); puts(*(p+1)); break; } if(!strcmp(*p, word)) break; p = p + 2; /* продвижение по списку */ } while(*p); if(!*p) puts("Слово в словаре отсутствует."); printf("Будете еще вводить? (y/n): "); scanf(" %c%*c", &ch); } while(toupper(ch) != 'N'); return 0; } Форматный ввод / вывод на консоль Функции printf() и scanf() выполняют форматный ввод и вывод, то есть они могут читать и писать данные в разных форматах. Данные на консоль выводит printf(). А функция scanf(), наоборот, считывает данные с клавиатуры. Обе функции могут работать с любым встроенным типом данных, а также с символьными строками, которые завершаются символом конца строки ('0'). Функция printf() Прототип функции printf(): int printf(const char *управляющая_строка, ...); Функция printf() возвращает число выведенных символов или отрицательное значение в случае ошибки. Управляющая_строка состоит из элементов двух видов. Первый из них — это символы, которые предстоит вывести на экран; второй — это спецификаторы преобразования, которые определяют способ вывода стоящих за ними аргументов. Каждый такой спецификатор начинается со знака процента, за которым следует код формата. Аргументов должно быть ровно столько, сколько и спецификаторов, причем спецификаторы преобразования и аргументы должны попарно соответствовать друг другу в направлении слева направо. Например, в результате такого вызова printf() printf("Мне нравится язык %c %s", 'C', "и к тому же очень сильно!"); Будет выведено Мне нравится язык C и к тому же очень сильно! В этом примере первому спецификатору преобразования (%c), соответствует символ 'C', а второму (%s), — строка "и к тому же очень сильно!". Таблица Спецификаторы преобразования для функции printf() Код Формат %a Шестнадцатеричное в виде 0xh.hhhhp+d (только С99) %A Шестнадцатеричное в виде 0Xh.hhhhP+d (только С99) %c Символ %d Десятичное целое со знаком %i Десятичное целое со знаком %e Экспоненциальное представление ('е' на нижнем регистре) %E Экспоненциальное представление ('Е' на верхнем регистре) %f Десятичное с плавающей точкой %g В зависимости от того, какой вывод будет короче, используется %е или %f %G В зависимости от того, какой вывод будет короче, используется %Е или %F %o Восьмеричное без знака %s Строка символов %u Десятичное целое без знака %x Шестнадцатеричное без знака (буквы на нижнем регистре) %X Шестнадцатеричное без знака (буквы на верхнем регистре) %p Выводит указатель %n Аргумент, соответствующий этому спецификатору, должен быть указателем на целочисленную переменную. Спецификатор позволяет сохранить в этой переменной количество записанных символов (записанных до того места, в котором находится код %n) %% Выводит знак % Вывод символов Для вывода отдельного символа используется %с. В результате соответствующий аргумент будет выведен на экран без изменения. Для вывода строки используется %s. Вывод чисел Числа в десятичном формате со знаком отображаются с помощью спецификатора преобразования %d или %i. Эти спецификаторы преобразования эквивалентны. Для вывода целого значения без знака используется %u. Спецификатор преобразования %f дает возможность выводить числа в формате с плавающей точкой. Соответствующий аргумент должен иметь тип double. Спецификаторы преобразования %e и %E в функции printf() позволяют отображать аргумент типа double в экспоненциальном формате. В общем виде числа в таком формате выглядят следующим образом: x.dddddE+/-yy Чтобы отобразить букву E в верхнем регистре, используется спецификатор преобразования %E; в противном случае используется спецификатор преобразования %e. Спецификатор преобразования %g или %G указывает, что функции printf() необходимо выбрать один из спецификаторов: %f или %e. В результате printf() выберет тот спецификатор преобразования, который позволяет сделать самый короткий вывод. Если нужно, чтобы при выборе экспоненциального формата буква E отображалась на верхнем регистре, используется спецификатор преобразования %G; в противном случае используется спецификатор преобразования %g. Применение спецификатора преобразования %g показано в следующей программе: #include int main(void) { double f; for(f=1.0; f<1.0e+10; f=f*10) printf("%g ", f); return 0; } В результате выполнения получится следующее: 1 10 100 1000 10000 100000 1e+06 1e+07 1e+08 1e+09 Целые числа без знака можно выводить в восьмеричном или шестнадцатеричном формате, используя спецификатор преобразования %o или %x. Так как в шестнадцатеричной системе для представления чисел от 10 до 15 используются буквы от А до F, то эти буквы можно выводить в верхнем или в нижнем регистре. В первом случае используется спецификатор преобразования %X, а во втором — спецификатор преобразования %x: #include int main(void) { unsigned num; for(num=0; num < 16; num++) { printf("%o ", num); printf("%x ", num); printf("%X\n", num); } return 0; } Вот что выведет программа: 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5 6 6 6 7 7 7 10 8 8 11 9 9 12 a A 13 b B 14 c C 15 d D 16 e E 17 f F Отображение адреса Для отображения адреса используется спецификатор преобразования %p. Этот спецификатор преобразования дает printf() указание отобразить машинный адрес в формате, совместимом с адресацией, которая используется компьютером. Следующая программа отображает адрес переменной sample: #include int sample; int main(void) { printf("%p", &sample); return 0; } Спецификатор преобразования %n Спецификатор %n довольно значительно отличается от остальных спецификаторов преобразования. Когда функция printf() встречает его, ничто не выводится. Вместо этого выполняется совсем другое действие: в целую переменную, указанную соответствующим аргументом функции, записывается количество выведенных символов. Другими словами, значение, которое соответствует спецификатору преобразования %n, должно быть указателем на переменную. После завершения вызова printf() в этой переменной будет храниться количество символов, выведенных до того момента, когда встретился спецификатор преобразования %n. Чтобы уяснить смысл этого несколько необычного спецификатора преобразования, разберитесь, как работает следующая программа: #include int main(void) { int count; printf("this%n is a test\n", &count); printf("%d", count); return 0; } Программа отображает строку Это проверка, после которой появляется число 3. Спецификатор преобразования %n в основном используется в программе для выполнения динамического форматирования. Модификаторы формата Во многих спецификаторах преобразования можно указать модификаторы, которые меняют их значение. Например, можно указывать минимальную ширину поля, количество десятичных разрядов и выравнивание по левому краю. Модификатор формата помещают между знаком процента и кодом формата. Модификаторы минимальной ширины поля Целое число, расположенное между знаком % и кодом формата, играет роль модификатора минимальной ширины поля. Если указан модификатор минимальной ширины поля, то чтобы ширина поля вывода была не меньше указанной минимальной длины, при необходимости вывод будет дополнен пробелами. Если же выводятся строки или числа, которые длиннее указанного минимума, то они все равно будут отображаться полностью. По умолчанию для дополнения используются пробелы. А если для этого надо использовать нули, то перед модификатором ширины поля следует поместить 0. Например, %05d означает, что любое число, количество цифр которого меньше пяти, будет дополнено таким количеством нулей, чтобы число состояло из пяти цифр. В следующей программе показано, как применяется модификатор минимальной ширины поля: #include int main(void) { double item; item = 10.12304; printf("%f\n", item); printf("%10f\n", item); printf("%012f\n", item); return 0; } Вот что выводится при выполнении этой программы: 10.123040 10.123040 00010.123040 Модификатор минимальной ширины поля чаще всего используется при создании таблиц, в которых столбцы должны быть выровнены по вертикали. Например, следующая программа выводит таблицу квадратов и кубов чисел от 1 до 19: #include int main(void) { int i; /* вывод таблицы квадратов и кубов */ for(i=1; i<20; i++) printf("%8d %8d %8d\n", i, i*i, i*i*i); return 0; } А вот пример полученного с ее помощью вывода: 1 1 1 2 4 8 3 9 27 4 16 64 5 25 125 6 36 216 7 49 343 8 64 512 9 81 729 10 100 1000 11 121 1331 12 144 1728 13 169 2197 14 196 2744 15 225 3375 16 256 4096 17 289 4913 18 324 5832 19 361 6859 Модификаторы точности Модификатор точности следует за модификатором минимальной ширины поля (если таковой имеется). Он состоит из точки и расположенного за ней целого числа. Значение этого модификатора зависит от типа данных, к которым его применяют. Когда модификатор точности применяется к данным с плавающей точкой, для преобразования которых используются спецификаторы преобразования %f, %e или %E, то он определяет количество выводимых десятичных разрядов. Например, %10.4f означает, что ширина поля вывода будет не менее 10 символов, причем для десятичных разрядов будет отведено четыре позиции. Если модификатор точности применяется к %g или %G, то он определяет количество значащих цифр. Примененный к строкам, модификатор точности определяет максимальную длину поля. Например, %5.7s означает, что длина выводимой строки будет составлять минимум пять и максимум семь символов. Если строка окажется длиннее, чем максимальная длина поля, то конечные символы выводиться не будут. Если модификатор точности применяется к целым типам, то он определяет минимальное количество цифр, которые будут выведены для каждого из чисел. Чтобы получилось требуемое количество цифр, добавляется некоторое количество ведущих нулей. В следующей программе показано, как можно использовать модификатор точности: #include int main(void) { printf("%.4f\n", 123.1234567); printf("%3.8d\n", 1000); printf("%10.15s\n", "Это простая проверка."); return 0; } Вот что выводится при выполнении этой программы: 123.1235 00001000 Выравнивание вывода По умолчанию весь вывод выравнивается по правому краю. То есть если ширина поля больше ширины выводимых данных, то эти данные располагаются по правому краю поля. Вывод по левому краю можно назначить принудительно, поместив знак минус прямо за %. Например, %-l0.2f означает, что число с плавающей точкой и с двумя десятичными разрядами будет выровнено по левому краю 10-символьного поля. В следующей программе показано, как применяется выравнивание по левому краю: #include int main(void) { printf(".........................\n"); printf("по правому краю: %8d\n", 100); printf(" по левому краю: %-8d\n", 100); return 0; } И вот что получилось: по правому краю: 100 по левому краю: 100 Обработка данных других типов Некоторые модификаторы в вызове функции printf() позволяют отображать целые числа типа short и long. Такие модификаторы можно использовать для следующих спецификаторов типа: d, i, o, u и x. Модификатор l (эль) в вызове функции printf() указывает, что за ним следуют данные типа long. Например, %ld означает, что надо выводить данные типа long int. После модификатора h функция printf() выведет целое значение в виде short. Например, %hu означает, что выводимые данные имеют тип short unsigned int. Модификаторы l и h можно также применить к спецификатору n. Это делается с той целью, чтобы показать — соответствующий аргумент является указателем соответственно на длинное (long) или короткое (short) целое. Если компилятор поддерживает обработку символов в расширенном 16-битном алфавите, добавленную Поправкой 1 от 1995 года (1995 Amendment 1), то для указания символа в расширенном 16-битном алфавите вы можете применять модификатор 1 для спецификатора преобразования c. Кроме того, для указания строки из символов в расширенном 16-битном алфавите можно применять модификатор 1 для спецификатора преобразования s. Модификатор L может находиться перед спецификаторами преобразования с плавающей точкой e, f и g, и указывать этим, что преобразуется значение long double. В Стандарте С99 вводится два новых модификатора формата: hh и ll. Модификатор hh можно применять для спецификаторов преобразования d, i, o, u, x или n. Он показывает, что соответствующий аргумент является значением signed или unsigned char или, в случае n, указателем на переменную signed char. Модификатор ll также можно применять для спецификаторов преобразования d, i, o, u, x или n. Он показывает, что соответствующий аргумент является значением signed или unsigned long long int или, в случае n, указателем на long long int. В С99 также разрешается применять l для спецификаторов преобразования с плавающей точкой a, е, f и g; впрочем, это не дает никакого результата. Модификатор * и # Для некоторых из своих спецификаторов преобразования функция printf() поддерживает два дополнительных модификатора: * и #. Непосредственное расположение # перед спецификаторами преобразования g, G, f, Е или e означает, что при выводе обязательно появится десятичная точка — даже если десятичных цифр нет. Если вы поставите # непосредственно перед x или X, то шестнадцатеричное число будет выведено с префиксом 0x. Если # будет непосредственно предшествовать спецификатору преобразования o, число будет выведено с ведущим нулем. К любым другим спецификаторам преобразования модификатор # применять нельзя. (В С99 модификатор # можно применять по отношению к преобразованию %а; это значит, что обязательно будет выведена десятичная точка.) Модификаторы минимальной ширины поля и точности можно передавать функции printf() не как константы, а как аргументы. Для этого в качестве заполнителя используется звездочку (*). При сканировании строки формата функция printf() будет каждой звездочке * из этой строки ставить в соответствие очередной аргумент, причем в том порядке, в каком расположены аргументы. Например, при выполнении оператора, показанного на рис. 8.1, минимальная ширина поля будет равна 10 символам, точность — 4, а отображаться будет число 123.3. В следующей программе показано применение обоих модификаторов # и *: #include int main(void) { printf("%x %#x\n", 10, 10); printf("%*.*f", 10, 4, 1234.34); return 0; } Функция scanf() Функция scanf() — это программа ввода общего назначения, выполняющая ввод с консоли. Она может читать данные всех встроенных типов и автоматически преобразовывать числа в соответствующий внутренний формат, scanf() во многом выглядит как обратная к printf(). Вот прототип функции scanf(): int scanf(const char *управляющая_строка, ...); Эта функция возвращает количество тех элементов данных, которым было успешно присвоено значение. В случае ошибки scanf() возвращает EOF, управляющая_строка определяет преобразование считываемых значений при записи их переменные, на которые указывают элементы списка аргументов. Управляющая строка состоит из символов трех видов: • спецификаторов преобразования, • разделителей, • символов, не являющихся разделителями. • Теперь поговорим о каждом из этих видов. Спецификаторы преобразования Каждый спецификатор формата ввода начинается со знака %, причем спецификаторы формата ввода сообщают функции scanf() тип считываемых данных. Перечень этих кодов (т.е. литер-спецификаторов) приведен в таблице. Спецификаторам преобразования в порядке слева направо ставятся в соответствие элементы списка аргументов. Рассмотрим некоторые примеры. Таблица Спецификаторы преобразования для функции scanf() Код Значение %a Читает значение с плавающей точкой (только С99) %c Читает одиночный символ %d Читает десятичное целое число %i Читает целое число как в десятичном, так и восьмеричном или шестнадцатеричном формате %e Читает число с плавающей точкой %f Читает число с плавающей точкой %g Читает число с плавающей точкой %о Читает восьмеричное число %s Читает строку %x Читает шестнадцатеричное число %p Читает указатель %n Принимает целое значение, равное количеству уже считанных символов %u Читает десятичное целое число без знака %[] Читает набор сканируемых символов %% Читает знак процента Ввод чисел Для чтения целого числа используется спецификатор преобразования %d или %i. A для чтения числа с плавающей точкой, представленного в стандартном или экспоненциальном виде, используется спецификатор преобразования %e, %f или %g. (Кроме того, для чтения числа с плавающей точкой стандарт С99 разрешает использовать также спецификатор преобразования %a.) Функцию scanf() можно использовать для чтения целых значений в восьмеричной или шестнадцатеричной форме, применяя для этого соответственно команды форматирования %o и %x, последняя из которых может быть как на верхнем, так и на нижнем регистре. Когда вводятся шестнадцатеричные числа, то буквы от А до F, представляющие шестнадцатеричные цифры, должны быть на том же самом регистре, что и литера-спецификатор. Следующая программа читает восьмеричное и шестнадцатеричное число: #include int main(void) { int i, j; scanf("%o%x", &i, &j); printf("%o %x", i, j); return 0; } Функция scanf() прекращает чтение числа тогда, когда встречается первый нечисловой символ. Ввод целых значений без знака Для ввода целого значения без знака используется спецификатор формата %u. Например, операторы unsigned num; scanf("%u", &num); выполняют считывание целого числа без знака и присваивают его переменной num. Чтение одиночных символов с помощью scanf() Одиночные символы можно прочитать с помощью функции getchar() или какой-либо функции, родственной с ней. Для той же цели можно использовать также вызов функции scanf() со спецификатором формата %c. Но, как и большинство реализаций getchar(), функция scanf() при использовании спецификатора преобразования %c обычно будет выполнять построчно буферизованный ввод. В интерактивной среде такая ситуация вызывает определенные трудности. При чтении одиночного символа символы разделителей читаются так же, как и любой другой символ, хотя при чтении данных других типов разделители интерпретируются как разделители полей. Например, при вводе с входного потока "x y" фрагмент кода scanf("%c%c%c", &a, &b, &c); помещает символ x в a, пробел — в b, а символ y — в c. Чтение строк Для чтения из входного потока строки можно использовать функцию scanf() со спецификатором преобразования %s. Использование спецификатора преобразования %s заставляет scanf() читать символы до тех пор, пока не встретится какой-либо разделитель. Что касается scanf(), то таким разделителем может быть пробел, разделитель строк, табуляция, вертикальная табуляция или подача страницы. В отличие от gets(), которая читает строку, пока не будет нажата клавиша , scanf() читает строку до тех пор, пока не встретится первый разделитель. Это означает, что scanf() нельзя использовать для чтения строки "это испытание", потому что после пробела процесс чтения прекратится. Читаемые символы помещаются в символьный массив, на который указывает соответствующий аргумент, а после введенных символов еще добавляется символ конца строки ('0'). Чтобы увидеть, как действует спецификатор %s, попробуйте при выполнении этой программы ввести строку "привет всем": #include int main(void) { char str[80]; printf("Введите строку: "); scanf("%s", str); printf("Вот Ваша строка: %s", str); return 0; } Программа выведет только часть строки, то есть слово привет. Ввод адреса Для ввода какого-либо адреса памяти используется спецификатор преобразования %p. Этот спецификатор преобразования заставляет функцию scanf() читать адрес в том формате, который определен архитектурой центрального процессора. Например, следующая программа вначале вводит адрес, а затем отображает то, что находится в памяти по этому адресу: #include int main(void) { char *p; printf("Введите адрес: "); scanf("%p", &p); printf("По адресу %p находится %c\n", p, *p); return 0; } Спецификатор %n Спецификатор %n указывает, что scanf() должна поместить количество символов, считанных (до того момента, когда встретился %n) из входного потока в целую переменную, указанную соответствующим аргументом. Использование набора сканируемых символов Функция scanf() поддерживает спецификатор формата общего назначения, называемый набором сканируемых символов (scanset). Набор сканируемых символов представляет собой множество символов. Когда scanf() обрабатывает такое множество, то вводит только те символы, которые входят в набор сканируемых символов. Читаемые символы будут помещаться в массив символов, который указан аргументом, соответствующим набору сканируемых символов. Этот набор определяется следующим образом: все те символы, которые предстоит сканировать, помещают в квадратные скобки. Непосредственно перед открывающей квадратной скобкой должен находиться знак %. Например, следующий набор сканируемых символов дает указание scanf() сканировать только символы X, Y и Z: %[XYZ] При использовании набора сканируемых символов функция scanf() продолжает читать символы, помещая их в соответствующий массив символов, пока не встретится символ, не входящий в этот набор. При возвращении из scanf() в массиве символов будет находиться строка, состоящая из считанных символов, причем эта строка будет заканчиваться символом конца строки. Чтобы увидеть, как это все работает, запустите следующую программу: #include int main(void) { int i; char str[80], str2[80]; scanf("%d%[abcdefg]%s", &i, str, str2); printf("%d %s %s", i, str, str2); return 0; } Введите 123abcdtye, а затем нажмите клавишу . После этого программа выведет 123 abed tye. Так как в данном случае 't' не входит в набор сканируемых символов, то scanf() прекратила чтение символов в переменную str сразу после того, как встретился символ 't'. Оставшиеся символы были помещены в переменную str2. Кроме того, можно указать набор сканируемых символов, работающий с точностью до наоборот; тогда первым символом в таком наборе должен быть ^. Этот символ дает указание scanf() принимать любой символ, который не входит в набор сканируемых символов. В большинстве реализаций для указания диапазона можно использовать дефис. Например, указанный ниже набор сканируемых символов дает функции scanf() указание принимать символы от А до Z: %[A-Z] Набор сканируемых символов чувствителен к регистру букв. Если нужно сканировать буквы и на верхнем, и на нижнем регистре, то их надо указывать отдельно для каждого регистра. Пропуск лишних разделителей Разделитель в управляющей строке дает scanf() указание пропустить в потоке ввода один или несколько начальных разделителей. Разделителями являются пробелы, табуляции, вертикальные табуляции, подачи страниц и разделители строк. В сущности, один разделитель в управляющей строке заставляет scanf() читать, но не сохранять любое количество (в том числе и нулевое) разделителей, которые находятся перед первым символом, не являющимся разделителем. Символы в управляющей строке, не являющиеся разделителями Если в управляющей строке находится символ, не являющийся разделителем, то функция scanf() прочитает символ из входного потока, проверит, совпадает ли прочитанный символ с указанным в управляющей строке, и в случае совпадения пропустит прочитанный символ. Например, "%d,%d" заставляет scanf() прочитать целое значение, прочитать запятую и пропустить ее (если это была запятая!), а затем прочитать следующее целое значение. Если же указанный символ во входном потоке не будет найден, то scanf() завершится. Когда нужно прочитать и отбросить знак процента, то в управляющей строке следует указать %%. Функции scanf() необходимо передавать адреса Для всех переменных, которые должны получить значения с помощью scanf(), должны быть переданы адреса. Это означает, что все аргументы должны быть указателями. Вспомните, что именно так в С создается вызов по ссылке и именно тогда функция может изменить содержимое аргумента. Например, для считывания целого значения в переменную count можно использовать такой вызов функции scanf(): scanf("%d", &count); Строки будут читаться в символьные массивы, а имя массива без индекса является адресом первого его элемента. Таким образом, чтобы прочитать строку в символьный массив, можно использовать оператор scanf("%s", str); В этом случае str является указателем, и потому перед ним не нужно ставить оператор &. Модификаторы формата Как и printf(), функция scanf() дает возможность модифицировать некоторое число своих спецификаторов формата. В спецификаторах формата моно указать модификатор максимальной длины поля. Это целое число, расположенное между % и спецификатором формата; оно ограничивает число символов, считываемых из этого поля. Например, чтобы считывать в переменную str не более 20 символов, пишите scanf("%20s", str); Если поток ввода содержит больше 20 символов, то при следующем вызове функций ввода считывание начнется после того места, где оно закончилось при предыдущем вызове. Например, если вы в ответ на вызов scanf() из этого примера введете ABCDEFGHIJKLMNOPRSTUVWXYZ то в str из-за спецификатора максимальной ширины поля будет помещено только 20 символов, то есть символы вплоть до Т. Это значит, что оставшиеся символы UVWXYZ пока еще не прочитаны. При следующем вызове scanf(), например при выполнении оператора scanf("%s", str); в str будут помешены буквы UVWXYZ. Ввод из поля может завершиться и до того, как будет достигнута максимальная длина поля — если встретится разделитель. В таком случае scanf() переходит к следующему полю. Чтобы прочитать длинное целое, перед спецификатором формата поместите l (эль). А для чтения короткого целого значения перед спецификатором формата следует поместить n. Эти модификаторы можно использовать со следующими кодами форматов: d, i, o, u, x и n. По умолчанию спецификаторы f, e и g дают scanf() указание присваивать данные переменной типа float. Если перед одним из этих спецификаторов будет помещен l (эль), то scanf() будет присваивать данные переменной типа double. Использование L дает scanf() указание, чтобы переменная, принимающая данные, имела тип long double. Если в компиляторе предусмотрена обработка двухбайтовых символов, добавленных в язык С Поправкой 1 от 1995 года, то модификатор l можно также использовать с такими кодами формата, как c и s. l непосредственно перед c является признаком указателя на объект типа wchar_t. А l непосредственно перед s — признак указателя на массив элементов типа wchar_t. Кроме того, l также применяется для модификации набора сканируемых символов, чтобы этот набор можно было использовать для двухбайтовых символов. В Стандарте С99, кроме перечисленных, предусмотрены также модификаторы ll и hh, последний из которых можно применять к спецификаторам d, i, o, u, x или n. Он является признаком того, что соответствующий аргумент является указателем на значение, типа signed или unsigned char. Кроме того, к спецификаторам d, i, o, u, x и n можно применять и ll, этот спецификатор является признаком того, что соответствующий аргумент является указателем на значение типа signed (или unsigned) long long int. Подавление ввода scanf() может прочитать поле, но не присваивать прочитанное значение никакой переменной; для этого надо перед литерой-спецификатором формата поля поставить звездочку, *. Например, когда выполняется оператор scanf("%d%*c%d", &x, &y); можно ввести пару координат 10,10. Запятая будет прочитана правильно, но ничему не будет присвоена. Подавление присвоения особенно полезно тогда, когда нужно обработать только часть того, что вводится. Управляющие символьные константы Среди управляющих символьных констант наиболее часто используются следующие: \а - для кратковременной подачи звукового сигнала; \b - для перевода курсора влево на одну позицию; \f - для подачи формата; \n - для перехода на новую строку; \r - для возврата каретки; \t - горизонтальная табуляция; \v - вертикальная табуляция; \\ - вывод символа \; \' - вывод символа ' ; \" - вывод символа "; \? - вывод символа ?. Например, в результате вызова функции: printf("\tComputer\n%d\n", i); сначала выполняется горизонтальная табуляция (\t), т.е. курсор сместится от края экрана, затем на экран будет выведено слово Computer, после этого курсор переместится в начало следующей строки (\n), затем будет выведено целое число i по формату %d (десятичное целое), и, окончательно, курсор перейдет в начало новой строки (\n). Напечатать строку символов можно и так: printf("Это строка символов"); Операнды и операции Комбинация знаков операций и операндов, результатом которой является определенное значение, называется выражением. Знаки операций определяют действия, которые должны быть выполнены над операндами. Каждый операнд в выражении может быть выражением. Значение выражения зависит от расположения знаков операций и круглых скобок в выражении, а также от приоритета выполнения операций. В языке СИ присваивание также является выражением, и значением такого выражения является величина, которая присваивается. При вычислении выражений тип каждого операнда может быть преобразован к другому типу. Преобразования типов могут быть неявными, при выполнении операций и вызовов функций, или явными, при выполнении операций приведения типов. Операнд – это: • константа, • литерал, • идентификатор, • вызов функции, • индексное выражение, • выражение выбора элемента • или более сложное выражение, сформированное комбинацией операндов, знаков операций и круглых скобок. Любой операнд, который имеет константное значение, называется константным выражением. Каждый операнд имеет тип. Если в качестве операнда используется константа, то ему соответствует значение и тип представляющей его константы. Целая константа может быть типа int, long, unsigned int, unsigned long, в зависимости от ее значения и от формы записи. Символьная константа имеет тип int. Константа с плавающей точкой всегда имеет тип double. Строковый литерал состоит из последовательности символов, заключенных в кавычки, и представляется в памяти как массив элементов типа char, инициализируемый указанной последовательностью символов. Значением строкового литерала является адрес первого элемента строки и синтаксически строковый литерал является немодифицируемым указателем на тип char. Строковые литералы могут быть использованы в качестве операндов в выражениях, допускающих величины типа указателей. Однако так как строки не являются переменными, их нельзя использовать в левой части операции присваивания. Последним символом строки всегда является нулевой символ, который автоматически добавляется при хранении строки в памяти. Идентификаторы переменных и функций. Каждый идентификатор имеет тип, который устанавливается при его объявлении. Значение идентификатора зависит от типа следующим образом: - идентификаторы объектов целых и плавающих типов представляют значения соответствующего типа; - идентификатор объекта типа enum представлен значением одной константы из множества значений констант в перечислении. Значением идентификатора является константное значение. Тип значения есть int, что следует из определения перечисления; - идентификатор объекта типа struct или union представляет значение, определенное структурой или объединением; - идентификатор, объявляемый как указатель, представляет указатель на значение, заданное в объявлении типа; - идентификатор, объявляемый как массив, представляет указатель, значение которого является адресом первого элемента массива. Тип адресуемых указателем величин - это тип элементов массива. Адрес массива не может быть изменен во время выполнения программы, хотя значение отдельных элементов может изменяться. Значение указателя, представляемое идентификатором массива, не является переменной и поэтому идентификатор массива не может появляться в левой части оператора присваивания. - идентификатор, объявляемый как функция, представляет указатель, значение которого является адресом функции, возвращающей значения определенного типа. Адрес функции не изменяется во время выполнения программы, меняется только возвращаемое значение. Таким образом, идентификаторы функций не могут появляться в левой части операции присваивания. Вызов функций состоит из выражения, за которым следует необязательный список выражений в круглых скобках: выражение-1 ([ список выражений ]) Значением выражения-1 должен быть адрес функции (например, идентификатор функции). Значения каждого выражения из списка выражений передается в функцию в качестве фактического аргумента. Операнд, являющийся вызовом функции, имеет тип и значение возвращаемого функцией значения. Индексное выражение задает элемент массива и имеет вид: выражение-1 [ выражение-2 ] Тип индексного выражения является типом элементов массива, а значение представляет величину, адрес которой вычисляется с помощью значений выражение-1 и выражение-2. Обычно выражение-1 - это указатель, например, идентификатор массива, а выражение-2 - это целая величина. Однако требуется только, чтобы одно из выражений было указателем, а второе целочисленной величиной. Поэтому выражение-1 может быть целочисленной величиной, а выражение-2 указателем. В любом случае выражение-2 должно быть заключено в квадратные скобки. Хотя индексное выражение обычно используется для ссылок на элементы массива, тем не менее, индекс может появляться с любым указателем. Индексные выражения для ссылки на элементы одномерного массива вычисляются путем сложения целой величины со значениями указателя с последующим применением к результату операции разадресации (*). Так как одно из выражений, указанных в индексном выражении, является указателем, то при сложении используются правила адресной арифметики, согласно которым целая величина преобразуется к адресному представлению, путем умножения ее на размер типа, адресуемого указателем. Пусть, например, идентификатор arr объявлен как массив элементов типа double. double arr[10]; Таким образом, чтобы получить доступ к i-тому элементу массива arr можно написать аrr[i], что эквивалентно i[a]. При этом величина i умножается на размер типа double и представляет собой адрес i-го элемента массива arr от его начала. Затем это значение складывается со значением указателя arr, что в свою очередь дает адрес i-го элемента массива. К полученному адресу применяется операция разадресации, т.е. осуществляется выборка элемента массива arr по сформированному адресу. Таким образом, результатом индексного выражения arr[i] (или i[arr]) является значение i-го элемента массива. Выражение с несколькими индексами ссылается на элементы многомерных массивов. Многомерный массив - это массив, элементами которого являются массивы. Например, первым элементом трехмерного массива является массив с двумя измерениями. Для ссылки на элемент многомерного массива индексное выражение должно иметь несколько индексов заключенных к квадратные скобки: выражение-1 [ выражение-2 ][ выражение-3 ] ... Такое индексное выражение интерпретируется слева направо, т.е. вначале рассматривается первое индексное выражение: выражение-1 [ выражение-2 ] Результат этого выражения есть адресное выражение, с которым складывается выражение-3 и т.д. Операция разадресации осуществляется после вычисления последнего индексного выражения. Операция разадресации не применяется, если значение последнего указателя адресует величину типа массива. Пример: int mass [2][5][3]; Рассмотрим процесс вычисления индексного выражения mass[1][2][2]. 1. Вычисляется выражения mass[1]. Ссылка индекс 1 умножается на размер элемента этого массива, элементом же этого массива является двухмерный массив содержащий 5х3 элементов, имеющих тип int. Получаемое значение складывается со значением указателя mass. Результат является указатель на второй двухмерный массив размером (5х3) в трехмерном массиве mass. 2. Второй индекс 2 указывает на размер массива из трех элементов типа int, и складывается с адресом, соответствующим mass [1]. 3. Так как каждый элемент трехмерного массива - это величина типа int, то индекс 2 увеличивается на размер типа int перед сложением с адресом mass [1][2]. 4. Наконец, выполняется разадресация полученного указателя. Результирующим выражением будет элемент типа int. Если было бы указано mass [1][2], то результатом был бы указатель на массив из трех элементов типа int. Соответственно значением индексного выражения mass [1] является указатель на двухмерный массив. Выражение выбора элемента применяется, если в качестве операнда надо использовать элемент структуры или объединения. Такое выражение имеет значение и тип выбранного элемента. Рассмотрим две формы выражения выбора элемента: выражение.идентификатор , выражение->идентификатор . В первой форме выражение представляет величину типа struct или union, а идентификатор - это имя элемента структуры или объединения. Во второй форме выражение должно иметь значение адреса структуры или объединения, а идентификатор - именем выбираемого элемента структуры или объединения. Обе формы выражения выбора элемента дают одинаковый результат. Действительно, запись, включающая знак операции выбора (->), является сокращенной версией записи с точкой для случая, когда выражению, стоящему перед точкой предшествует операция разадресации (*), примененная к указателю, т.е. запись выражение -> идентификатор эквивалентна записи (* выражение) . идентификатор в случае, если выражение является указателем. Пример: struct tree { float num; int spisoc[5]; struct tree *left;} tr[5] , elem ; elem.left = & elem; В приведенном примере используется операция выбора (.) для доступа к элементу left структурной переменной elem. Таким образом, элементу left структурной переменной elem присваивается адрес самой переменной elem, т.е. переменная elem хранит ссылку на себя саму. Приведение типов Приведение типов это изменение (преобразование) типа объекта. Для выполнения преобразования необходимо перед объектом записать в скобках нужный тип: ( имя-типа ) операнд. Приведение типов используются для преобразования объектов одного скалярного типа в другой скалярный тип. Однако выражению с приведением типа не может быть присвоено другое значение. Пример: int i; bouble x; b = (double)i+2.0; В этом примере целая переменная i с помощью операции приведения типов приводится к плавающему типу, а затем уже участвует в вычислении выражения. Константное выражение - это выражение, результатом которого является константа. Операндом константного выражения могут быть целые константы, символьные константы, константы с плавающей точкой, константы перечисления, выражения приведения типов, выражения с операцией sizeof и другие константные выражения. Однако на использование знаков операций в константных выражениях налагаются следующие ограничения: 1. в константных выражениях нельзя использовать операции присваивания и последовательного вычисления (,); 2. операция "адрес" (&) может быть использована только при некоторых инициализациях; 3. выражения со знаками операций могут участвовать в выражениях как операнды. Выражения со знаками операций могут быть унарными (с одним операндом), бинарными (с двумя операндами) и тернарными (с тремя операндами); Унарное выражение состоит из операнда и предшествующего ему знаку унарной операции и имеет следующий формат: знак-унарной-операции операнд Бинарное выражения состоит из двух операндов, разделенных знаком бинарной операции: операнд1 знак-бинарной-операции операнд2 Тернарное выражение состоит из трех операндов, разделенных знаками тернарной операции (?) и (:), и имеет формат: операнд1 ? операнд2 : операнд3 . Операции По количеству операндов, участвующих в операции, операции подразделяются на • унарные; • бинарные; • тернарные. В языке Си имеются следующие унарные операции: 1. - арифметическое отрицание (отрицание и дополнение); 2. ~ побитовое логическое отрицание (дополнение); 3. ! логическое отрицание; 4. * разадресация (косвенная адресация); 5. & вычисление адреса; 6. + унарный плюс; 7. ++ увеличение (инкремент); 8. -- уменьшение (декремент); 9. sizeof размер. Унарные операции выполняются справа налево. Операции увеличения и уменьшения увеличивают или уменьшают значение операнда на единицу и могут быть записаны как справа, так и слева от операнда. Если знак операции записан перед операндом (префиксная форма), то изменение операнда происходит до его использования в выражении. Если знак операции записан после операнда (постфиксная форма), то операнд вначале используется в выражении, а затем происходит его изменение. В отличие от унарных, бинарные операции, список которых приведен в таблице, выполняются слева направо. Знак операции Операция Группа операций * Умножение Мультипликативные / Деление % Остаток от деления + Сложение Аддитивные - Вычитание << Сдвиг влево Операции сдвига >> Сдвиг вправо < Меньше Операции отношения <= Меньше или равно >= Больше или равно == Равно != Не равно & Поразрядное И Поразрядные операции | Поразрядное ИЛИ ^ Поразрядное исключающее ИЛИ && Логическое И Логические операции || Логическое ИЛИ , Последовательное вычисление Последовательного вычисления = Присваивание Операции присваивания *= Умножение с присваиванием /= Деление с присваиванием %= Остаток от деления с присваиванием -= Вычитание с присваиванием += Сложение с присваиванием <<= Сдвиг влево с присваиванием >>= Сдвиг вправо присваиванием &= Поразрядное И с присваиванием |= Поразрядное ИЛИ с присваиванием ^= Поразрядное исключающее ИЛИ с присваиванием Левый операнд операции присваивания должен быть выражением, ссылающимся на область памяти (но не объектом объявленным с ключевым словом const), такие выражения называются леводопустимыми. К ним относятся: 1. идентификаторы данных целого и плавающего типов, типов указателя, структуры, объединения; 2. индексные выражения, исключая выражения имеющие тип массива или функции; 3. выражения выбора элемента (->) и (.), если выбранный элемент является леводопустимым; 4. выражения унарной операции разадресации (*), за исключением выражений, ссылающихся на массив или функцию; 5. выражение приведения типа если результирующий тип не превышает размера первоначального типа. При записи выражений следует помнить, что символы (*), (&), (!), (+) могут обозначать унарную или бинарную операцию. Преобразования при вычислении выражений При выполнении операций производится автоматическое преобразование типов, чтобы привести операнды выражений к общему типу или чтобы расширить короткие величины до размера целых величин, используемых в машинных командах. Выполнение преобразования зависит от специфики операций и от типа операнда или операндов. Общие арифметические преобразования: 1. Операнды типа float преобразуются к типу double. 2. Если один операнд long double, то второй преобразуется к этому же типу. 3. Если один операнд double, то второй также преобразуется к типу double. 4. Любые операнды типа char и short преобразуются к типу int. 5. Любые операнды unsigned char или unsigned short преобразуются к типу unsigned int. 6. Если один операнд типа unsigned long, то второй преобразуется к типу unsigned long. 7. Если один операнд типа long, то второй преобразуется к типу long. 8. Если один операнд типа unsigned int, то второй операнд преобразуется к этому же типу. Таким образом, можно отметить, что при вычислении выражений операнды преобразуются к типу того операнда, который имеет наибольший размер. Пример: double ft,sd; unsigned char ch; unsigned long in; int i; .... sd=ft*(i+ch/in); При выполнении оператора присваивания правила преобразования будут использоваться следующим образом. 1. операнд ch преобразуется к unsigned int (правило 5); 2. затем он преобразуется к типу unsigned long (правило 6); 3. по этому же правилу i преобразуется к unsigned long и результат операции, заключенной в круглые скобки будет иметь тип unsigned long; 4. затем он преобразуется к типу double (правило 3) и результат всего выражения будет иметь тип double. Операции отрицания и дополнения Операция арифметического отрицания (-) вырабатывает отрицание своего операнда. Операнд должен быть целой или плавающей величиной. При выполнении осуществляются обычные арифметические преобразования. Пример: double u = 5; u = -u; /* переменной u присваивается ее отрицание, т.е. u принимает значение -5*/ Операция логического отрицания "НЕ" (!) вырабатывает значение 0, если операнд есть истина (не нуль), и значение 1, если операнд равен нулю (0). Результат имеет тип int. Операнд должен быть целого или плавающего типа или типа указатель. Пример: int t, z=0; t=!z; Переменная t получит значение равное 1, так как переменная z имела значение равное 0 (ложно). Операция двоичного дополнения (~) вырабатывает двоичное дополнение своего операнда. Операнд должен быть целого типа. Осуществляется обычное арифметическое преобразование, результат имеет тип операнда после преобразования. Пример: сhar b = '9'; unsigned char f; b = ~f; Шестнадцатеричное значение символа '9' равно 39. В результате операции ~f будет получено шестнадцатеричное значение С6, что соответствует символу 'ц'. Операции разадресации и адреса Эти операции используются для работы с переменными типа указатель. Операция разадресации (*) осуществляет косвенный доступ к адресуемой величине через указатель. Операнд должен быть указателем. Результатом операции является величина, на которую указывает операнд. Типом результата является тип величины, адресуемой указателем. Результат не определен, если указатель содержит недопустимый адрес. Рассмотрим типичные ситуации, когда указатель содержит недопустимый адрес: - указатель является нулевым; - указатель определяет адрес такого объекта, который не является активным в момент ссылки; - указатель определяет адрес, который не выровнен до типа объекта, на который он указывает; - указатель определяет адрес, не используемый выполняющейся программой. Операция адрес (&) дает адрес своего операнда. Операндом может быть любое именуемое выражение. Имя функции или массива также может быть операндом операции "адрес", хотя в этом случае знак операции является лишним, так как имена массивов и функций являются адресами. Результатом операции адрес является указатель на операнд. Тип, адресуемый указателем, является типом операнда. Операция адрес не может применяться к элементам структуры, являющимися полями битов, и к объектам с классом памяти register. Примеры: int t, f=0, * adress; adress = &t /* переменной adress, объявляемой как указатель, присваивается адрес переменной t */ * adress =f; /* переменной находящейся по адресу, содержащемуся в переменной adress, присваивается значение переменной f, т.е. 0 , что эквивалентно t=f; т.е. t=0;*/ Операция sizeof С помощью операции sizeof можно определить размер памяти, которая соответствует идентификатору или типу. Операция sizeof имеет следующий формат: sizeof(выражение) В качестве выражения может быть использован любой идентификатор, либо имя типа, заключенное в скобки. Но не может быть использовано имя типа void, а идентификатор не может относиться к полю битов или быть именем функции. Если в качестве выражения указанно имя массива, то результатом является размер всего массива (т.е. произведение числа элементов на длину типа), а не размер указателя, соответствующего идентификатору массива. Когда sizeof применяются к имени типа структуры или объединения или к идентификатору имеющему тип структуры или объединения, то результатом является фактический размер структуры или объединения, который может включать участки памяти, используемые для выравнивания элементов структуры или объединения. Таким образом, этот результат может не соответствовать размеру, получаемому путем сложения размеров элементов структуры. Пример: struct { char h; int b; double f; } str; int a1; a1 = sizeof(str); Переменная а1 получит значение, равное 12, в то же время если сложить длины всех используемых в структуре типов, то получим, что длина структуры str равна 7. Несоответствие имеет место в виду того, что после размещения в памяти первой переменной h длинной 1 байт, добавляется 1 байт для выравнивания адреса переменной b на границу слова (слово имеет длину 2 байта для машин серии IBM PC AT /286/287), далее осуществляется выравнивание адреса переменной f на границу двойного слова (4 байта). Таким образом, в результате операций выравнивания, для размещения структуры в оперативной памяти требуется на пять байт больше. В связи с этим целесообразно рекомендовать при объявлении структур и объединения располагать их элементы в порядке убывания длины типов: struct { double f; int b; char h; } str; Мультипликативные операции К этому классу операций относятся операции 1. умножения (*), 2. деления (/) 3. и получение остатка от деления (%). Операндами операции (%) должны быть целые числа. Типы операндов операций умножения и деления могут отличаться, и для них справедливы правила преобразования типов. Типом результата является тип операндов после преобразования. Операция умножения (*) выполняет умножение операндов. int i=5; float f=0.2; double g,z; g=f*i; Тип произведения i и f преобразуется к типу double, затем результат присваивается переменной g. Операция деления (/) выполняет деление первого операнда на второй. Если две целые величины не делятся нацело, то результат округляется в сторону нуля. При попытке деления на ноль выдается сообщение во время выполнения. int i=49, j=10, n, m; n = i/j; /* результат 4 */ m = i/(-j); /* результат -4 */ Операция остаток от деления (%) дает остаток от деления первого операнда на второй. Знак результата зависит от конкретной реализации. В данной реализации знак результата совпадает со знаком делимого. Если второй операнд равен нулю, то выдается сообщение. int n = 49, m = 10, i, j, k, l; i = n % m; /* 9 */ j = n % (-m); /* 9 */ k = (-n) % m; /* -9 */ l = (-n) % (-m); /* -9 */ Аддитивные операции К аддитивным операциям относятся сложение (+) и вычитание (-). Операнды могут быть целого или плавающего типов. В некоторых случаях над операндами аддитивных операций выполняются общие арифметические преобразования. Однако преобразования, выполняемые при аддитивных операциях, не обеспечивают обработку ситуаций переполнения и потери значимости. Информация теряется, если результат аддитивной операции не может быть представлен типом операндов после преобразования. При этом сообщение об ошибке не выдается. Пример: int i=30000, j=30000, k; k=i+j; В результате сложения k получит значение равное -5536. Результатом выполнения операции сложения является сумма двух операндов. Операнды могут быть целого или плавающего типа или один операнд может быть указателем, а второй - целой величиной. Когда целая величина складывается с указателем, то целая величина преобразуется путем умножения ее на размер памяти, занимаемой величиной, адресуемой указателем. Когда преобразованная целая величина складывается с величиной указателя, то результатом является указатель, адресующий ячейку памяти, расположенную на целую величину дальше от исходного адреса. Новое значение указателя адресует тот же самый тип данных, что и исходный указатель. Операция вычитания (-) вычитает второй операнд из первого. Возможна следующая комбинация операндов: 1. Оба операнда целого или плавающего типа. 2. Оба операнда являются указателями на один и тот же тип. 3. Первый операнд является указателем, а второй - целым. Операции сложения и вычитания над адресами в единицах, отличных от длины типа, могут привести к непредсказуемым результатам. Пример: double d[10],* u; int i; u = d+2; /* u указывает на третий элемент массива */ i = u-d; /* i принимает значение равное 2 */ Операции сдвига Операции сдвига осуществляют смещение операнда влево (<<) или вправо (>>) на число битов, задаваемое вторым операндом. Оба операнда должны быть целыми величинами. Выполняются обычные арифметические преобразования. При сдвиге влево правые освобождающиеся биты устанавливаются в нуль. При сдвиге вправо метод заполнения освобождающихся левых битов зависит от типа первого операнда. Если тип unsigned, то свободные левые биты устанавливаются в нуль. В противном случае они заполняются копией знакового бита. Результат операции сдвига не определен, если второй операнд отрицательный. Преобразования, выполненные операциями сдвига, не обеспечивают обработку ситуаций переполнения и потери значимости. Информация теряется, если результат операции сдвига не может быть представлен типом первого операнда, после преобразования. Сдвиг влево соответствует умножению первого операнда на степень числа 2, равную второму операнду, а сдвиг вправо соответствует делению первого операнда на 2 в степени, равной второму операнду. Примеры: int i=0x1234, j, k ; k = i<<4 ; /* k="0x0234" */ j="i<<8" ; /* j="0x3400" */ i="j">>8 ; /* i = 0x0034 */ Поразрядные операции К поразрядным операциям относятся: операция поразрядного логического "И" (&), операция поразрядного логического "ИЛИ" (|), операция поразрядного "исключающего ИЛИ" (^). Операнды поразрядных операций могут быть любого целого типа. При необходимости над операндами выполняются преобразования по умолчанию, тип результата - это тип операндов после преобразования. Операция поразрядного логического И (&) сравнивает каждый бит первого операнда с соответствующим битом второго операнда. Если оба сравниваемых бита единицы, то соответствующий бит результата устанавливается в 1, в противном случае в 0. Операция поразрядного логического ИЛИ (|) сравнивает каждый бит первого операнда с соответствующим битом второго операнда. Если любой (или оба) из сравниваемых битов равен 1, то соответствующий бит результата устанавливается в 1, в противном случае результирующий бит равен 0. Операция поразрядного исключающего ИЛИ (^) сравнивает каждый бит первого операнда с соответствующими битами второго операнда. Если один из сравниваемых битов равен 0, а второй бит равен 1, то соответствующий бит результата устанавливается в 1, в противном случае, т.е. когда оба бита равны 1 или 0, бит результата устанавливается в 0. Пример. int i=0x45FF, /* i= 0100 0101 1111 1111 */ j=0x00FF; j= 0000 0000 1111 1111 */ char r; r = i^j; /* r=0x4500 = 0100 0101 0000 0000 */ r = i|j; /* r=0x45FF = 0100 0101 0000 0000 */ r = i&j /* r=0x00FF = 0000 0000 1111 1111 */ Логические операции К логическим операциям относятся операция логического И (&&) и операция логического ИЛИ (||). Операнды логических операций могут быть целого типа, плавающего типа или типа указателя, при этом в каждой операции могут участвовать операнды различных типов. Операнды логических выражений вычисляются слева направо. Если значения первого операнда достаточно, чтобы определить результат операции, то второй операнд не вычисляется. Логические операции не вызывают стандартных арифметических преобразований. Они оценивают каждый операнд с точки зрения его эквивалентности нулю. Результатом логической операции является 0 или 1, тип результата int. Операция логического И (&&) вырабатывает значение 1, если оба операнда имеют нулевые значения. Если один из операндов равен 0, то результат также равен 0. Если значение первого операнда равно 0, то второй операнд не вычисляется. Операция логического ИЛИ (||) выполняет над операндами операцию включающего ИЛИ. Она вырабатывает значение 0, если оба операнда имеют значение 0, если какой-либо из операндов имеет ненулевое значение, то результат операции равен 1. Если первый операнд имеет ненулевое значение, то второй операнд не вычисляется. Операция последовательного вычисления Операция последовательного вычисления обозначается запятой (,) и используется для вычисления двух и более выражений там, где по синтаксису допустимо только одно выражение. Эта операция вычисляет два операнда слева направо. При выполнении операции последовательного вычисления, преобразование типов не производится. Операнды могут быть любых типов. Результат операции имеет значения и тип второго операнда. Запятая может использоваться также как символ разделитель, поэтому необходимо по контексту различать, запятую, используемую в качестве разделителя или знака операции. Условная операция В языке СИ имеется одна тернарная операция - условная операция, которая имеет следующий формат: операнд-1 ? операнд-2 : операнд-3 Операнд-1 должен быть целого или плавающего типа или быть указателем. Он оценивается с точки зрения его эквивалентности 0. Если операнд-1 не равен 0, то вычисляется операнд-2 и его значение является результатом операции. Если операнд-1 равен 0, то вычисляется операнд-3 и его значение является результатом операции. Вычисляется либо операнд-2, либо операнд-3, но не оба. Тип результата зависит от типов операнда-2 и операнда-3, следующим образом. 1. Если операнд-2 или операнд-3 имеет целый или плавающий тип (их типы могут отличаться), то выполняются обычные арифметические преобразования. Типом результата является тип операнда после преобразования. 2. Если операнд-2 и операнд-3 имеют один и тот же тип структуры, объединения или указателя, то тип результата будет тем же самым типом структуры, объединения или указателя. 3. Если оба операнда имеют тип void, то результат имеет тип void. 4. Если один операнд является указателем на объект любого типа, а другой операнд является указателем на void, то указатель на объект преобразуется к указателю на void, который и будет типом результата. 5. Если один из операндов является указателем, а другой константным выражением со значением 0, то типом результата будет тип указателя. Пример: max = (d<=b) ? b : d; Переменной max присваивается максимальное значение переменных d и b. Операции увеличения и уменьшения Операции увеличения (++) и уменьшения (--) являются унарными операциями присваивания. Они соответственно увеличивают или уменьшают значения операнда на единицу. Операнд может быть целого или плавающего типа или типа указатель и должен быть модифицируемым. Операнд целого или плавающего типа увеличиваются (уменьшаются) на единицу. Тип результата соответствует типу операнда. Операнд адресного типа увеличивается или уменьшается на размер объекта, который он адресует. В языке допускается префиксная или постфиксная формы операций увеличения (уменьшения), поэтому значения выражения, использующего операции увеличения (уменьшения) зависит от того, какая из форм указанных операций используется. Если знак операции стоит перед операндом (префиксная форма записи), то изменение операнда происходит до его использования в выражении и результатом операции является увеличенное или уменьшенное значение операнда. В том случае если знак операции стоит после операнда (постфиксная форма записи), то операнд вначале используется для вычисления выражения, а затем происходит изменение операнда. Примеры: int t=1, s=2, z, f; z=(t++)*5; Вначале происходит умножение t*5, а затем увеличение t. В результате получится z=5, t=2. f=(++s)/3; Вначале значение s увеличивается, а затем используется в операции деления. В результате получим s=3, f=1. В случае, если операции увеличения и уменьшения используются как самостоятельные операторы, префиксная и постфиксная формы записи становятся эквивалентными. z++; /* эквивалентно */ ++z; Простое присваивание Операция простого присваивания используется для замены значения левого операнда, значением правого операнда. При присваивании производится преобразование типа правого операнда к типу левого операнда по правилам, упомянутым раньше. Левый операнд должен быть модифицируемым. Пример: int t; char f; long z; t=f+z; Значение переменной f преобразуется к типу long, вычисляется f+z ,результат преобразуется к типу int и затем присваивается переменной t. Составное присваивание Кроме простого присваивания, имеется целая группа операций присваивания, которые объединяют простое присваивание с одной из бинарных операций. Такие операции называются составными операциями присваивания и имеют вид: (операнд-1) (бинарная операция) = (операнд-2) . Составное присваивание по результату эквивалентно следующему простому присваиванию: (операнд-1) = (операнд-1) (бинарное операция) (операнд-2) . Выражение составного присваивания с точки зрения реализации не эквивалентно простому присваиванию, так как в последнем операнд-1 вычисляется дважды. Каждая операция составного присваивания выполняет преобразования, которые осуществляются соответствующей бинарной операцией. Левым операндом операций (+=) (-=) может быть указатель, в то время как правый операнд должен быть целым числом. Примеры: double arr[4]={ 2.0, 3.3, 5.2, 7.5 } ; double b=3.0; b+=arr[2]; /* эквивалентно b=b+arr[2]*/ arr[3]/=b+1; /* эквивалентно arr[3]=arr[3]/(b+1) */ Заметим, что при втором присваивании использование составного присваивания дает более заметный выигрыш во времени выполнения, так как левый операнд является индексным выражением. Приоритеты операций и порядок вычислений В языке СИ операции с высшими приоритетами вычисляются первыми. Наивысшим приоритетом является приоритет равный 1. Приоритет Знак операции Типы операции Порядок выполнения 1 - ~ ! * & ++ -- sizeof приведение типов Унарные Справа налево 2 () [] . -> Выражение Слева направо 3 * / % Мультипликативные Слева направо 4 + - Аддитивные 5 << >> Сдвиг 6 < > <= >= Отношение 7 == != Отношение (равенство) 8 & Поразрядное И 9 ^ Поразрядное исключающее ИЛИ 10 | Поразрядное ИЛИ 11 && Логическое И 12 || Логическое ИЛИ 13 ? : Условная 14 = *= /= %= += -= &= |= >>= <<= ^= Простое и составное присваивание Справа налево 15 , Последовательное вычисление Слева направо Побочные эффекты Операции присваивания в сложных выражениях могут вызывать побочные эффекты, так как они изменяют значение переменной. Побочный эффект может возникать и при вызове функции, если он содержит прямое или косвенное присваивание (через указатель). Это связано с тем, что аргументы функции могут вычисляться в любом порядке. Например, побочный эффект имеет место в следующем вызове функции: prog (a,a=k*2); В зависимости от того, какой аргумент вычисляется первым, в функцию могут быть переданы различные значения. Порядок вычисления операндов некоторых операций зависит от реализации и поэтому могут возникать разные побочные эффекты, если в одном из операндов используется операции увеличения или уменьшения, а также другие операции присваивания. Например, выражение i*j+(j++)+(--i) может принимать различные значения при обработке разными компиляторами. Чтобы избежать недоразумений при выполнении побочных эффектов необходимо придерживаться следующих правил. 1. Не использовать операции присваивания переменной в вызове функции, если эта переменная участвует в формировании других аргументов функции. 2. Не использовать операции присваивания переменной в выражении, если эта переменная используется в выражении более одного раза. 7 Типы данных: Концепция типов данных. Основные типы данных Типы данных и их объявление Важным отличием языка СИ от других языков (PL1, FORTRAN, и др.) является отсутствие принципа умолчания, что приводит к необходимости объявления всех переменных используемых в программе явно вместе с указанием соответствующих их типов. Объявления переменной имеет следующий формат: [спецификатор-класа-памяти] спецификатор-типа описатель [=инициатор] [,описатель [= инициатор] ]... Описатель - идентификатор простой переменной либо более сложная конструкция с квадратными скобками, круглыми скобками или звездочкой (набором звездочек). Спецификатор типа - одно или несколько ключевых слов, определяющих тип объявляемой переменной. В языке СИ имеется стандартный набор типов данных, используя который можно сконструировать новые (уникальные) типы данных. Инициатор - задает начальное значение или список начальных значений, которые (которое) присваивается переменной при объявлении. Спецификатор класса памяти - определяется одним из четырех ключевых слов языка СИ: auto, extern, register, static, и указывает,каким образом будет распределяться память под объявляемую переменную, с одной стороны, а с другой, область видимости этой переменной, т.е., из каких частей программы можно к ней обратиться. Категории типов данных Стандарт С89 определяет пять фундаментальных типов данных: 1. char — символьные данные, 2. int — целые, 3. float — с плавающей точкой, 4. double — двойной точности, 5. void — без значения. На основе этих типов формируются другие типы данных. Размер (объем занимаемой памяти) и диапазон значений этих типов данных для разных процессоров и компиляторов могут быть разными. Однако объект типа char всегда занимает 1 байт. Размер объекта int обычно совпадает с размером слова в конкретной среде программирования. В большинстве случаев в 16-разрядной среде (DOS или Windows 4.1) int занимает 16 битов, а в 32-разрядной (Windows 95/98/NT/2000) — 32 бита. Однако полностью полагаться на это нельзя, особенно при переносе программы в другую среду. Необходимо помнить, что стандарт С обусловливает только минимальный диапазон значений каждого типа данных, но не размер в байтах. Конкретный формат числа с плавающей точкой зависит от его реализации в трансляторе. Переменные типа char обычно используются для обозначения набора символов стандарта ASCII, символы, не входящие в этот набор, разными компиляторами обрабатываются по-разному. Диапазон значений типов float и double зависит от формата представления чисел с плавающей точкой. Стандарт С определяет для чисел с плавающей точкой минимальный диапазон значений от 1Е-37 до 1Е+37. Тип void служит для объявления функции, не возвращающей значения, или для создания универсального указателя. Модификация базовых типов Базовые типы данных (кроме void) могут иметь различные спецификаторы, предшествующие им в тексте программы. Спецификатор типа так изменяет значение базового типа, чтобы он более точно соответствовал своему назначению в программе. Полный список спецификаторов типов: signed unsigned long short Базовый тип int может быть модифицирован каждым из этих спецификаторов. Тип char модифицируется с помощью unsigned и signed, double — с помощью long. (Стандарт С99 также позволяет модифицировать long с помощью long, создавая таким образом long long, см. часть II). В табл. 2.1 приведены все допустимые комбинации типов данных с их минимальным диапазоном значений и типичным размером. В таблице приведены минимально возможные, а не типичные диапазоны значений. Например, если в компьютере арифметические операции выполняются над числами в дополнительных кодах (а именно так спроектированы почти все компьютеры!), то в диапазон значений целых попадут все целые числа от -32767 до 32768. Тип Типичный размер в битах Минимально допустимый диапозон значений char 8 от -127 до 127 unsigned char 8 от 0 до 255 signed char 8 от -127 до 127 int 16 или 32 от -32767 до 32767 unsigned int 16 или 32 от 0 до 65535 signed int 16 или 32 то же, что int short int 16 от -32767 до 32767 unsigned short int 16 от 0 до 65535 signed short int 16 то же, что short int long int 32 от -2 147 483 647 до 2 147 483 647 long long int 64 от -(263-1) до (263-1), добавлен стандартом C99 signed long int 32 то же, что long int unsigned long int 32 от 0 до 4 294 967 295 unsigned long long int 64 от 0 до (264-1), добавлен в C99 float 32 от 1E-37 до 1E+37, с точностью не менее 6 значащих десятичных цифр double 64 от 1E-37 до 1E+37, с точностью не менее 10 значащих десятичных цифр long double 80 от 1E-37 до 1E+37, с точностью не менее 10 значащих десятичных цифр Для целых можно использовать спецификатор signed, но в этом нет необходимости, потому что при объявлении целого он предполагается по умолчанию. Спецификатор signed чаще всего используется для типа char, который в некоторых реализациях по умолчанию может быть беззнаковым Если спецификатор типа записать сам по себе (без следующего за ним базового типа), то предполагается, что он модифицирует тип int. Хотя базовый тип int и предполагается по умолчанию, его, тем не менее, обычно указывают явно. Ключевые слова signed и unsigned указывают, как интерпретируется нулевой бит объявляемой переменной. Если указано ключевое слово unsigned, то нулевой бит интерпретируется как часть числа, в противном случае нулевой бит интерпретируется как знаковый. В случае отсутствия ключевого слова unsigned целая переменная считается знаковой. В том случае, если спецификатор типа состоит из ключевого типа signed или unsigned и далее следует идентификатор переменной, то она будет рассматриваться как переменная типа int. Например: unsigned int n; unsigned int b; int c; (подразумевается signed int c ); unsigned d; (подразумевается unsigned int d ); signed f; (подразумевается signed int f ). Модификатор-типа char используется для представления символа (из массива представление символов) или для объявления строковых литералов. Значением объекта типа char является код (размером 1 байт), соответствующий представляемому символу. Для представления символов русского алфавита, модификатор типа идентификатора данных имеет вид unsigned char, так как коды русских букв превышают величину 127. В языке СИ не определено представление в памяти и диапазон значений для идентификаторов с модификаторами-типа int и unsigned int. Размер памяти для переменной с модификатором типа signed int определяется длиной машинного слова, которое имеет различный размер на разных машинах. Так, на 16-ти разрядных машинах размер слова равен 2-м байтам, на 32-х разрядных машинах соответственно 4-м байтам, т.е. тип int эквивалентен типам short int, или long int в зависимости от архитектуры используемой ПЭВМ. Таким образом, одна и та же программа может правильно работать на одном компьютере и неправильно на другом. Для определения длины памяти занимаемой переменной можно использовать операцию sizeof языка СИ, возвращающую значение длины указанного модификатора-типа. Например: a = sizeof(int); b = sizeof(long int); c = sizeof(unsigned long); d = sizeof(short); Восьмеричные и шестнадцатеричные константы также могут иметь модификатор unsigned. Это достигается указанием префикса u или U после константы, константа без этого префикса считается знаковой. Например: 0xA8C (int signed); 01786l (long signed); 0xF7u (int unsigned ); Переменные в формате ПЗ Для переменных, представляющих число с плавающей точкой используются следующие модификаторы-типа : float, double, long double (в некоторых реализациях языка long double СИ отсутствует). Величина с модификатором-типа float занимает 4 байта. Из них 1 байт отводится для знака, 8 бит для избыточной экспоненты и 23 бита для мантиссы. Старший бит мантиссы всегда равен 1, поэтому он не заполняется, в связи с этим диапазон значений переменной с плавающей точкой приблизительно равен от 3.14E-38 до 3.14E+38. Величина типа double занимает 8 бит в памяти. Ее формат аналогичен формату float. Биты памяти распределяются следующим образом: 1 бит для знака, 11 бит для экспоненты и 52 бита для мантиссы. С учетом опущенного старшего бита мантиссы диапазон значений равен от 1.7E-308 до 1.7E+308. Примеры: float f, a, b; double x,y; Указатели Указатель - это адрес памяти, распределяемой для размещения идентификатора (в качестве идентификатора может выступать имя переменной, массива, структуры, строкового литерала). В том случае, если переменная объявлена как указатель, то она содержит адрес памяти, по которому может находится скалярная величина любого типа. При объявлении переменной типа указатель, необходимо определить тип объекта данных, адрес которых будет содержать переменная, и имя указателя с предшествующей звездочкой (или группой звездочек). Формат объявления указателя: спецификатор-типа [ модификатор ] * описатель. Спецификатор-типа задает тип объекта и может быть любого основного типа, типа структуры, смеси (об этом будет сказано ниже). Задавая вместо спецификатора-типа ключевое слово void, можно своеобразным образом отсрочить спецификацию типа, на который ссылается указатель. Переменная, объявляемая как указатель на тип void, может быть использована для ссылки на объект любого типа. Однако для того, чтобы можно было выполнить арифметические и логические операции над указателями или над объектами, на которые они указывают, необходимо при выполнении каждой операции явно определить тип объектов. Такие определения типов может быть выполнено с помощью операции приведения типов. В качестве модификаторов при объявлении указателя могут выступать ключевые слова const, near, far, huge. Ключевое слово const указывает, что указатель не может быть изменен в программе. Размер переменной объявленной как указатель, зависит от архитектуры компьютера и от используемой модели памяти, для которой будет компилироваться программа. Указатели на различные типы данных не обязательно должны иметь одинаковую длину. Для модификации размера указателя можно использовать ключевые слова near, far, huge. Примеры: unsigned int * a; /* переменная а представляет собой указатель на тип unsigned int (целые числа без знака) */ double * x; /* переменная х указывает на тип данных с плавающей точкой удвоенной точности */ char * fuffer ; /* объявляется указатель с именем fuffer который указывает на переменную типа char */ double nomer; void *addres; addres = & nomer; (double *)addres ++; /* Переменная addres объявлена как указатель на объект любого типа. Поэтому ей можно присвоить адрес любого объекта (& - операция вычисления адреса). Ни одна арифмитическая операция не может быть выполнена над указателем, пока не будет явно определен тип данных, на которые он указывает. Это можно сделать, используя операцию приведения типа (double *) для преобразования addres к указателю на тип double, а затем увеличение адреса. */const * dr; /* Переменная dr объявлена как указатель на константное выражение, т.е. значение указателя может изменяться в процессе выполнения программы, а величина, на которую он указывает, нет. */unsigned char * const w = &obj. /* Переменная w объявлена как константный указатель на данные типа char unsigned. Это означает, что на протяжение всей программы w будет указывать на одну и ту же область памяти. Содержание же этой области может быть изменено. */ Переменные перечислимого типа Переменная, которая может принимать значение из некоторого списка значений, называется переменной перечислимого типа или перечислением. Объявление перечисления начинается с ключевого слова enum и имеет два формата представления. Формат 1. enum [имя-тега-перечисления] {список-перечисления} описатель[,описатель...]; Формат 2. enum имя-тега-перечисления описатель [,описатель..]; Объявление перечисления задает тип переменной перечисления и определяет список именованных констант, называемый списком-перечисления. Значением каждого имени списка является некоторое целое число. Переменная типа перечисления может принимать значения одной из именованных констант списка. Именованные константы списка имеют тип int. Таким образом, память соответствующая переменной перечисления, это память необходимая для размещения значения типа int. Переменная типа enum могут использоваться в индексных выражениях и как операнды в арифметических операциях и в операциях отношения. В первом формате 1 имена и значения перечисления задаются в списке перечислений. Необязательное имя-тега-перечисления, это идентификатор, который именует тег перечисления, определенный списком перечисления. Описатель именует переменную перечисления. В объявлении может быть задана более чем одна переменная типа перечисления. Список-перечисления содержит одну или несколько конструкций вида: идентификатор [= константное выражение] Каждый идентификатор именует элемент перечисления. Все идентификаторы в списке enum должны быть уникальными. В случае отсутствия константного выражения первому идентификатору соответствует значение 0, следующему идентификатору - значение 1 и т.д. Имя константы перечисления эквивалентно ее значению. Идентификатор, связанный с константным выражением, принимает значение, задаваемое этим константным выражением. Константное выражение должно иметь тип int и может быть как положительным, так и отрицательным. Следующему идентификатору в списке присваивается значение, равное константному выражению плюс 1, если этот идентификатор не имеет своего константного выражения. Использование элементов перечисления должно подчиняться следующим правилам: 1. Переменная может содержать повторяющиеся значения. 2. Идентификаторы в списке перечисления должны быть отличны от всех других идентификаторов в той же области видимости, включая имена обычных переменных и идентификаторы из других списков перечислений. 3. Имена типов перечислений должны быть отличны от других имен типов перечислений, структур и смесей в этой же области видимости. 4. Значение может следовать за последним элементом списка перечисления. Пример: enum week { SUB = 0, /* 0 */ VOS = 0, /* 0 */ POND, /* 1 */ VTOR, /* 2 */ SRED, /* 3 */ HETV, /* 4 */ PJAT /* 5 */ } rab_ned ; В данном примере объявлен перечислимый тег week, с соответствующим множеством значений, и объявлена переменная rab_ned имеющая тип week. Во втором формате используется имя тега перечисления для ссылки на тип перечисления, определяемый где-то в другом месте. Имя тега перечисления должно относится к уже определенному тегу перечисления в пределах текущей области видимости. Так как тег перечисления объявлен где-то в другом месте, список перечисления не представлен в объявлении. Пример: enum week rab1; В объявлении указателя на тип данных перечисления и объявляемых typedef для типов перечисления можно использовать имя тега перечисления до того, как данный тег перечисления определен. Однако определение перечисления должно предшествовать любому действию используемого указателя на тип объявления typedef. Объявление без последующего списка описателей описывает тег, или, если так можно сказать, шаблон перечисления. Определение объектов и типов Все переменные, используемые в программах на языке СИ, должны быть объявлены. Тип объявляемой переменной зависит от того, какое ключевое слово используется в качестве спецификатора типа и является ли описатель простым идентификатором или же комбинацией идентификатора с модификатором указателя (звездочка), массива (квадратные скобки) или функции (круглые скобки). При объявлении простой переменной, структуры, смеси или объединения, а также перечисления, описатель - это простой идентификатор. Для объявления указателя, массива или функции идентификатор модифицируется соответствующим образом: звездочкой слева, квадратными или круглыми скобками справа. При объявлении можно использовать одновременно более одного модификатора, что дает возможность создавать множество различных сложных описателей типов. Однако надо помнить, что некоторые комбинации модификаторов недопустимы: - элементами массивов не могут быть функции, - функции не могут возвращать массивы или функции. При инициализации сложных описателей квадратные и круглые скобки (справа от идентификатора) имеют приоритет перед звездочкой (слева от идентификатора). Квадратные или круглые скобки имеют один и тот же приоритет и раскрываются слева направо. Спецификатор типа рассматривается на последнем шаге, когда описатель уже полностью проинтерпретирован. Можно использовать круглые скобки, чтобы поменять порядок интерпретации на необходимый. Для интерпретации сложных описаний предлагается простое правило, которое звучит как "изнутри наружу", и состоит из четырех шагов. 1. Начать с идентификатора и посмотреть вправо, есть ли квадратные или круглые скобки. 2. Если они есть, то проинтерпретировать эту часть описателя и затем посмотреть налево в поиске звездочки. 3. Если на любой стадии справа встретится закрывающая круглая скобка, то вначале необходимо применить все эти правила внутри круглых скобок, а затем продолжить интерпретацию. 4. Интерпретировать спецификатор типа. Примеры: int * ( * comp [10] ) (); 6 5 3 1 2 4 В данном примере объявляется переменная comp (1), как массив из десяти (2) указателей (3) на функции (4), возвращающие указатели (5) на целые значения (6). char *( *( * var ) ()) [10]; 7 6 4 2 1 3 5 Переменная var (1) объявлена как указатель (2) на функцию (3) возвращающую указатель (4) на массив (5) из 10 элементов, которые являются указателями (6) на значения типа char. Кроме объявлений переменных различных типов, имеется возможность объявить типы. Это можно сделать двумя способами. Первый способ - указать имя тега при объявлении структуры, объединения или перечисления, а затем использовать это имя в объявлении переменных и функций в качестве ссылки на этот тег. Второй - использовать для объявления типа ключевое слово typedef. При объявлении с ключевым словом typedef, идентификатор, стоящий на месте описываемого объекта, является именем вводимого в рассмотрение типа данных, и далее этот тип может быть использован для объявления переменных. Любой тип может быть объявлен с использованием ключевого слова typedef, включая типы указателя, функции или массива. Имя с ключевым словом typedef для типов указателя, структуры, объединения может быть объявлено, прежде чем эти типы будут определенны, но в пределах видимости объявителя. Примеры: typedef double (* MATH)( ); /* MATH - новое имя типа, представляющее указатель на функцию, возвращающую значения типа double */ MATH cos; /* cos указатель на функцию, возвращающую значения типа double */ /* Можно провести эквивалентное объявление */ double (* cos)( ); typedef char FIO[40] /* FIO - массив из сорока символов */ FIO person; /* Переменная person - массив из сорока символов */ /* Это эквивалентно объявлению */ char person[40]; При объявлении переменных и типов здесь были использованы имена типов (MATH FIO). Помимо этого, имена типов могут еще использоваться в трех случаях: в списке формальных параметров, в объявлении функций, в операциях приведения типов и в операции sizeof (операция приведения типа). Именами типов для основных типов, типов перечисления, структуры и смеси являются спецификаторы типов для этих типов. Имена типов для типов указателя массива и функции задаются при помощи абстрактных описателей следующим образом: спецификатор-типа абстрактный-описатель; Абстрактный-описатель - это описатель без идентификатора, состоящий из одного или более модификаторов указателя, массива или функции. Модификатор указателя (*) всегда задается перед идентификатором в описателе, а модификаторы массива [] и функции () - после него. Таким образом, чтобы правильно интерпретировать абстрактный описатель, нужно начать интерпретацию с подразумеваемого идентификатора. Абстрактные описатели могут быть сложными. Скобки в сложных абстрактных описателях задают порядок интерпретации подобно тому, как это делалось при интерпретации сложных описателей в объявлениях. Инициализация данных При объявлении переменной ей можно присвоить начальное значение, присоединяя инициатор к описателю. Инициатор начинается со знака "=" и имеет следующие формы. Формат 1: = инициатор; Формат 2: = { список - инициаторов }; Формат 1 используется при инициализации переменных основных типов и указателей, а формат 2 - при инициализации составных объектов. Примеры: char tol = 'N'; Переменная tol инициализируется символом 'N'. const long megabyte = (1024 * 1024); Немодифицируемая переменная megabyte инициализируется константным выражением после чего она не может быть изменена. static int b[2][2] = {1,2,3,4}; Инициализируется двухмерный массив b целых величин. Элементам массива присваиваются значения из списка. Эта же инициализация может быть выполнена следующим образом : static int b[2][2] = { { 1,2 }, { 3,4 } }; При инициализации массива можно опустить одну или несколько размерностей static int b[3[] = { { 1,2 }, { 3,4 } }; Если при инициализации указано меньше значений для строк, то оставшиеся элементы инициализируются 0, т.е. при описании static int b[2][2] = { { 1,2 }, { 3 } }; элементы первой строки получат значения 1 и 2, а второй 3 и 0. При инициализации составных объектов, нужно внимательно следить за использованием скобок и списков инициализаторов. Примеры: struct complex { double real; double imag; } comp [2][3] = { { {1,1}, {2,3}, {4,5} }, { {6,7}, {8,9}, {10,11} } }; В данном примере инициализируется массив структур comp из двух строк и трех столбцов, где каждая структура состоит из двух элементов real и imag. struct complex comp2 [2][3] = { {1,1},{2,3},{4,5}, {6,7},{8,9},{10,11} }; В этом примере компилятор интерпретирует рассматриваемые фигурные скобки следующим образом: - первая левая фигурная скобка - начало составного инициатора для массива comp2; - вторая левая фигурная скобка - начало инициализации первой строки массива comp2[0]. Значения 1,1 присваиваются двум элементам первой структуры; - первая правая скобка (после 1) указывает компилятору, что список инициаторов для строки массива окончен, и элементы оставшихся структур в строке comp[0] автоматически инициализируются нулем; - аналогично список {2,3} инициализирует первую структуру в строке comp[1], а оставшиеся структуры массива обращаются в нули; - на следующий список инициализаторов {4,5} компилятор будет сообщать о возможной ошибке так как строка 3 в массиве comp2 отсутствует. При инициализации объединения задается значение первого элемента объединения в соответствии с его типом. Пример: union tab { unsigned char name[10]; int tab1; } pers = {'A','H','T','O','H'}; Инициализируется переменная pers.name, и так как это массив, для его инициализации требуется список значений в фигурных скобках. Первые пять элементов массива инициализируются значениями из списка, остальные нулями. Инициализацию массива символов можно выполнить путем использования строкового литерала. char stroka[ ] = "привет"; Инициализируется массив символов из 7 элементов, последним элементом (седьмым) будет символ '\0', которым завершаются все строковые литералы. В том случае, если задается размер массива, а строковый литерал длиннее, чем размер массива, то лишние символы отбрасываются. Следующее объявление инициализирует переменную stroka как массив, состоящий из семи элементов. char stroka[5] = "привет"; В переменную stroka попадают первые пять элементов литерала, а символы 'Т' и '\0' отбрасываются. Если строка короче, чем размер массива, то оставшиеся элементы массива заполняются нулями. Инициализация переменной типа tab может иметь следующий вид: union tab pers1 = "Антон"; и, таким образом, в символьный массив попадут символы: 'А','Н','Т','О','Н','\0', а остальные элементы будут инициализированы нулем. 8 Преобразование типов Если в выражении появляются операнды различных типов, то они преобразуются к некоторому общему типу, при этом к каждому арифметическому операнду применяется такая последовательность правил: Если один из операндов в выражении имеет тип long double, то остальные тоже преобразуются к типу long double. В противном случае, если один из операндов в выражении имеет тип double, то остальные тоже преобразуются к типу double. В противном случае, если один из операндов в выражении имеет тип float, то остальные тоже преобразуются к типу float. В противном случае, если один из операндов в выражении имеет тип unsigned long, то остальные тоже преобразуются к типу unsigned long. В противном случае, если один из операндов в выражении имеет тип long, то остальные тоже преобразуются к типу long. В противном случае, если один из операндов в выражении имеет тип unsigned, то остальные тоже преобразуются. к типу unsigned. В противном случае все операнды преобразуются к типу int. При этом тип char преобразуется в int со знаком; тип unsigned char в int, у которого старший байт всегда нулевой; тип signed char в int, у которого в знаковый разряд передается знак из сhar; тип short в int (знаковый или беззнаковый). Предположим, что вычислено значение некоторого выражения в правой части оператора присваивания. В левой части оператора присваивания записана некоторая переменная, причем ее тип отличается от типа результата в правой части. Здесь правила преобразования очень простые: значение справа от оператора присваивания преобразуется к типу переменной слева от оператора присваивания. Если размер результата в правой части больше размера операнда в левой части, то старшая часть этого результата будет потеряна. В языке Си можно явно указать тип любого выражения. Для этого используется операция преобразования ("приведения") типа. Она применяется следующим образом: (тип) выражение (здесь можно указать любой допустимый в языке Си тип). Рассмотрим пример: int a = 30000; float b; ........ b = (float) a * 12; (переменная a целого типа явно преобразована к типу float; если этого не сделать, то результат будет потерян, т.к. a * 12 > 32767). Преобразование типа также может использоваться для преобразования типов аргументов при вызове функций. Неявное преобразование типов При выполнении операций происходят неявные преобразования типов в следующих случаях: - при выполнении операций осуществляются обычные арифметические преобразования ; - при выполнении операций присваивания, если значение одного типа присваивается переменной другого типа; - при передаче аргументов функции. Кроме того, в Си есть возможность явного приведения значения одного типа к другому. В операциях присваивания тип значения, которое присваивается, преобразуется к типу переменной, получающей это значение. Допускается преобразования целых и плавающих типов, даже если такое преобразование ведет к потере информации. Преобразование целых типов со знаком Целое со знаком преобразуется к более короткому целому со знаком, посредством усечения старших битов. Целое со знаком преобразуется к более длинному целому со знаком, путем размножения знака. При преобразовании целого со знаком к целому без знака, целое со знаком преобразуется к размеру целого без знака и результат рассматривается как значение без знака. Преобразование целого со знаком к плавающему типу происходит без потери информации, за исключением случая преобразования значения типа long int или unsigned long int к типу float, когда точность часто может быть потеряна. Преобразование целых типов без знака Целое без знака преобразуется к более короткому целому без знака или со знаком путем усечения старших битов. Целое без знака преобразуется к более длинному целому без знака или со знаком путем дополнения нулей слева. Когда целое без знака преобразуется к целому со знаком того же размера, битовое представление не изменяется. Поэтому значение, которое оно представляет, изменяется, если знаковый бит установлен (равен 1), т.е. когда исходное целое без знака больше чем максимальное положительное целое со знаком, такой же длины. Целые значения без знака преобразуются к плавающему типу, путем преобразования целого без знака к значению типа signed long, а затем значение signed long преобразуется в плавающий тип. Преобразования из unsigned long к типу float, double или long double производятся с потерей информации, если преобразуемое значение больше, чем максимальное положительное значение, которое может быть представлено для типа long. Преобразования плавающих типов Величины типа float преобразуются к типу double без изменения значения. Величины double и long double преобразуются к float c некоторой потерей точности. Если значение слишком велико для float, то происходит потеря значимости, о чем сообщается во время выполнения. При преобразовании величины с плавающей точкой к целым типам она сначала преобразуется к типу long (дробная часть плавающей величины при этом отбрасывается), а затем величина типа long преобразуется к требуемому целому типу. Если значение слишком велико для long, то результат преобразования не определен. Преобразования из float, double или long double к типу unsigned long производится с потерей точности, если преобразуемое значение больше, чем максимально возможное положительное значение, представленное типом long. Преобразование типов указателя Указатель на величину одного типа может быть преобразован к указателю на величину другого типа. Однако результат может быть не определен из-за отличий в требованиях к выравниванию и размерах для различных типов. Указатель на тип void может быть преобразован к указателю на любой тип, и указатель на любой тип может быть преобразован к указателю на тип void без ограничений. Значение указателя может быть преобразовано к целой величине. Метод преобразования зависит от размера указателя и размера целого типа следующим образом: - если размер указателя меньше размера целого типа или равен ему, то указатель преобразуется точно так же, как целое без знака; - если указатель больше, чем размер целого типа, то указатель сначала преобразуется к указателю с тем же размером, что и целый тип, и затем преобразуется к целому типу. Целый тип может быть преобразован к адресному типу по следующим правилам: - если целый тип того же размера, что и указатель, то целая величина просто рассматривается как указатель (целое без знака); - если размер целого типа отличен от размера указателя, то целый тип сначала преобразуется к размеру указателя, а затем полученное значение трактуется как указатель. Лекция 7_1 Выражения и операции Операции языка Си Любое выражение языка состоит из операндов (переменных, констант и др.), соединенных знаками операций. Знак операции - это символ или группа символов, которые сообщают компилятору о необходимости выполнения определенных арифметических, логических или других действий. Язык С содержит большое количество встроенных операций. Их роль в С значительно больше, чем в других языках программирования. Существует четыре основных класса операций: 1. арифметические, 2. логические, 3. поразрядные 4. и операции сравнения. Кроме них, есть также некоторые специальные операторы, например, оператор присваивания. Оператор присваивания Оператор присваивания может присутствовать в любом выражении языка С. Этим С отличается от большинства других языков программирования (Pascal, BASIC и FORTRAN), в которых присваивание возможно только в отдельном операторе. Общая форма оператора присваивания: имя_переменной=выражение; Выражение может быть просто константой или сколь угодно сложным выражением. В отличие от Pascal или Modula-2, в которых для присваивания используется знак ":=", в языке С оператором присваивания служит единственный знак присваивания "=". Адресатом (получателем), т.е. левой частью оператора присваивания должен быть объект, способный получить значение, например, переменная. В книгах по С и в сообщениях компилятора часто встречаются термины lvalue (left side value) и rvalue (right side value). Попросту говоря, lvalue — это объект. Если этот объект может стоять в левой части присваивания, то он называется также модифицируемым (modifiable) lvalue. Подытожим сказанное: lvalue — это объект в левой части оператора присваивания, получающий значение, чаще всего этим объектом является переменная. Термин rvalue означает значение выражения в правой части оператора присваивания. Преобразование типов при присваиваниях Если в операции встречаются переменные разных типов, происходит преобразование типов. В операторе присваивания действует простое правило: значение выражения в правой части преобразуется к типу объекта в левой части. int x; char ch; float f; void func(void) { ch = x; /* 1-я строка */ x = f; /* 2-я строка */ f = ch; /* 3-я строка */ f = x; /* 4-я строка */ } В 1-й строке этого примера старшие двоичные разряды целой переменной х отбрасываются, а в ch заносятся младшие 8 бит. Если значение х лежит в интервале от 0 до 255, то ch и х будут идентичны и потери информации не произойдет. В противном случае в ch будут занесены только младшие разряды переменной х. Во 2-й строке в х будет записана целая часть числа f. В 3-й строке произойдет преобразование целого 8-разрядного числа, хранящегося в ch, в число в плавающем формате. В 4-й строке произойдет то же самое, только с 16-разрядным целым. Преобразование целых в символы и длинных целых в целые удаляет соответствующее количество старших двоичных разрядов. В 16-разрядной среде теряются 8 битов при преобразовании целого в символ и 16 битов при преобразовании длинного целого в целое. В 32-разрядной среде теряются 24 бита при преобразовании целого в символ и 16 битов при преобразовании целого в короткое целое. В табл приведены варианты потери информации при некоторых преобразованиях. Необходимо помнить, что преобразование int во float или float в double не повышает точность вычислений. При таком преобразовании только изменяется форма представления числа. Некоторые компиляторы при преобразовании char в int считают переменную char положительной независимо от ее значения. Другие компиляторы считают переменную char отрицательной, если она больше 127. Поэтому для обеспечения переносимости программы необходимо использовать переменные типа char для хранения символов, а переменные типа signed char и int (целый) — для хранения чисел. Тип адресата Тип выражения Потеря информации signed char char Если значение > 127, то результат отрицательный char short int Старшие 6 бит char int (16-разрядный) Старшие 8 бит char int (32-разрядный) Старшие 24 бит char long int Старшие 24 бит short int int (16-разрядный) Нет short int int (32-разрядный) Старшие 16 бит int (16-разрядный) long int Старшие 16 бит int (32-разрядный) long int Нет long int (32-разрядный) long long int (64-разрядный) Старшие 32 бита (это относится только к C99) int float Дробная часть float double Результат округляется double long double Результат округляется Если какое-либо преобразование не приведено в табл., то, чтобы определить, что именно теряется в результате этого преобразования, нужно представить его в виде композиции (суперпозиции, произведения) указанных в таблице преобразований и затем провести последовательные преобразования. Например, преобразование double в int эквивалентно последовательному выполнению двух преобразований: сначала double в float, а затем float в int. Множественные присваивания В одном операторе присваивания можно присвоить одно и то же значение многим переменным. Для этого используется оператор множественного присваивания, например: x = y = z = 0; В практике программирования этот прием используется очень часто. Составное присваивание Составное присваивание — это разновидность оператора присваивания, в которой запись сокращается и становится более удобной в написании. Например, оператор x = x+10; можно записать как x += 10; Оператор "+=" сообщает компилятору, что к переменной х нужно прибавить 10. "Составные" операторы присваивания существуют для всех бинарных операций (то есть операций, имеющих два операнда). Любой оператор вида переменная = переменная оператор выражение; можно записать как переменная оператор = выражение; Еще один пример: x = x-100; означает то же самое, что и x -= 100; Составное присваивание значительно компактнее, чем соответствующее простое присваивание, поэтому его иногда называют стенографическим (shorthand) присваиванием. В программах на С этот оператор широко используется, поэтому необходимо хорошо его усвоить. Арифметические операции В табл. приведены арифметические операции С. Операции +, —, * и / работают так же, как и в большинстве других языков программирования. Их можно применять почти ко всем встроенным типам данных. Если операция / применяется к целому или символьному типам, то остаток от деления отбрасывается. Например, результатом операции 5/2 является 2. Оператор Операция - Вычитание, так же унарный минус + Сложение * Умножение / Деление % Остаток от деления -- Декремент, или уменьшение ++ Инкремент, или увеличение Оператор деления по модулю % в С работает так же, как и в других языках, его результатом является остаток от целочисленного деления. Этот оператор, однако, нельзя применять к типам данных с плавающей точкой. Применение оператора % иллюстрируется следующим примером: int x, y; x = 5; y = 2; printf("%d ", x/y); /* напечатает 2 */ printf("%d ", x%y); /* напечатает 1, остаток от целочисленного деления */ x = 1; y = 2; printf("%d %d", x/y, x%y); /*напечатает 0 1 */ Последняя строка программы напечатает 0 1 потому, что при целочисленном делении остаток отбрасывается и здесь результат будет 0, а сам остаток равен 1. Унарный минус умножает операнд на -1, то есть меняет его знак на противоположный. Операции увеличения (инкремента) и уменьшения (декремента) В языке С есть два полезных оператора, значительно упрощающие широко распространенные операции. Это инкремент ++ и декремент --. Оператор ++ увеличивает значение операнда на 1, а — уменьшает на 1. Иными словами, x = x+1; можно записать как ++x; Аналогично оператор x = x-1; равносилен оператору x--; Как инкремент, так и декремент могут предшествовать операнду (префиксная форма) или следовать за ним (постфиксная форма). Например x = x+1; можно записать как в виде ++x; так и в виде x++; Однако префиксная и постфиксная формы отличаются при использовании их в выражениях. Если оператор инкремента или декремента предшествует операнду, то сама операция выполняется до использования результата в выражении. Если же оператор следует за операндом, то в выражении значение операнда используется до выполнения операции инкремента или декремента. То есть для выражения эта операция как бы не существует, она выполняется только для операнда. Например, x = 10; y = ++x; присваивает у значение 11. Однако если написать x = 10; y = x++; то переменной у будет присвоено значение 10. В обоих случаях х присвоено значение 11, разница только в том, когда именно это случилось, до или после присваивания значения переменной у. Большинство компиляторов С генерируют для инкремента и декремента очень быстрый, эффективный объектный код, значительно лучший, чем для соответствующих операторов присваивания. Поэтому везде, где это возможно, рекомендуется использовать инкремент и декремент. Приоритет выполнения арифметических операторов следующий: Наивысший ++ -- - (унарный минус) * / % Наинизший + - Операции с одинаковым приоритетом выполняются слева направо. Используя круглые скобки, можно изменить порядок вычислений. В языке С круглые скобки интерпретируются компилятором так же, как и в любом другом языке программирования: они как бы придают операции (или последовательности операций) наивысший приоритет. Операции сравнения и логические операции Операции сравнения — это операции, в которых значения двух переменных сравниваются друг с другом. Логические же операции реализуют средствами языка С операции формальной логики. Между логическими операциями и операциями сравнения существует тесная связь: результаты операций сравнения часто являются операндами логических операций. В операциях сравнения и логических операциях в качестве операндов и результатов операций используются значения ИСТИНА (true) и ЛОЖЬ (false). В языке С значение ИСТИНА представляется любым числом, отличным от нуля. Значение ЛОЖЬ представляется нулем. Результатом операции сравнения или логической операции являются ИСТИНА (true, 1) или ЛОЖЬ (false, 0). Как в С89, так и в С99 значение ИСТИНА представлено любым отличным от нуля числом, а ЛОЖЬ — нулем. Полный список операций сравнения и логических операций: таблица истинности логических операций имеет следующий вид: p q p && q p || q !p 1 1 1 1 1 1 1 1 1 1 Как операции сравнения, так и логические операции имеют низший приоритет по сравнению с арифметическими. То есть, выражение 10>1+12 интерпретируется как 10>(1+12). Результат, конечно, равен ЛОЖЬ. В одном выражении можно использовать несколько операций: 10>5 && !(10<9) || 3<4 В этом случае результатом будет ИСТИНА. В языке С не определена операция "исключающего ИЛИ" (exclusive OR, или XOR). Однако с помощью логических операторов несложно написать функцию, выполняющую эту операцию. Результатом операции "исключающее ИЛИ" является ИСТИНА, если и только если один из операндов (но не оба) имеют значение ИСТИНА. В следующем примере функция xor() возвращает результат операции "исключающее ИЛИ", а операндами служат аргументы функции: #include int xor(int a, int b); int main(void) { printf("%d", xor(1, 0)); printf("%d", xor(1, 1)); printf("%d", xor(0, 1)); printf("%d", xor(0, 0)); return 0; } /* Выполнение логической оперции исключающее ИЛИ над двумя аргументами. */ int xor(int a, int b) { return (a || b) && !(a && b); } Операторы сравнения Оператор Операция > Больше чем >= Больше или равно < Меньше чем <= Меньше или равно == Равно != Не равно Логические операции Оператор Операция && И || ИЛИ ! НЕ, отрицание Ниже приведен приоритет логических операций: Наивысший ! > >= < <= == != && Наинизший || Как и в арифметических выражениях, для изменения порядка выполнения операций сравнения и логических операций можно использовать круглые скобки. Например, выражение: !0 && 0 || 0 равно ЛОЖЬ. Однако, если добавить скобки, то результатом будет ИСТИНА: !(0 && 0) || 0 Необходимо помнить, что результатом любой операции сравнения или логической операции есть 0 или 1. Поэтому следующий фрагмент программы является правильным и в результате его выполнения будет напечатано 1. int x; x = 100; printf("%d", x>10); Поразрядные операции В отличие от многих других языков программирования, в С определен полный набор поразрядных операций. Это обусловлено тем, что С был задуман как язык, призванный во многих приложениях заменить ассемблер, который способен оперировать битами данных. Поразрядные операции — это тестирование (проверка), сдвиг или присвоение значений отдельным битам данных. Эти операции осуществляются над ячейками памяти, содержащими данные типа char или int. Данные типа float, double, long double, void или другие более сложные не могут участвовать в поразрядных операциях. В табл. приведен полный список знаков поразрядных операций, выполняемых над отдельными разрядами (битами) операндов. Оператор Операция & И | ИЛИ ^ исключающее ИЛИ ~ НЕ (отрицание, дополнение к 1) >> Сдвиг вправо << Сдвиг влево Таблицы истинности логических операций и поразрядных операций И, ИЛИ, НЕ совпадают. Отличие лишь в том, что поразрядные операции выполняются над отдельными разрядами (битами) операндов. Операция "исключающее ИЛИ" имеет следующую таблицу истинности: p q p ^ q 1 1 1 1 1 1 Как показано в таблице, результат операции "исключающее ИЛИ" равен ИСТИНА если и только если один из операндов равен 1, иначе результат будет равен ЛОЖЬ. Примеры: если a = 0000 1111 и b = 1000 1000, то ~a = 1111 0000, a << 1 = 0001 1110, a >> 1 = 0000 0111, a & b = 0000 1000, a ^ b = 1000 0111, a | b = 1000 1111. Наиболее часто поразрядные операции применяются при программировании драйверов устройств, таких как модемы, а также процедур, выполняющих операции над файлами, и стандартных программ обслуживания принтера. В них поразрядные операции используются для маскирования определенных битов, например, бита контроля четности. (Этот бит служит для проверки правильности остальных битов в байте. Чаще всего это бит старшего разряда в каждом байте.) Операция И может быть использована для очищения бита. Иными словами, для гашения бита используется следующее свойство операции И: если бит одного из операндов равен 0, то соответствующий бит результата будет равен 0 независимо от состояния этого бита во втором операнде. Например, следующая функция читает символ из порта модема и обнуляет бит контроля четности: char get_char_from_modem(void) { char ch; ch = read_modem(); /* чтение символа из порта модема */ return(ch & 127); } Бит контроля четности, находящийся в 8-м разряде байта, обнуляется с помощью операции И. При этом в качестве второго операнда выбирается число, имеющее 1 в разрядах от 1 до 7, и 0 в 8-м разряде. Именно таким числом и является 127, поскольку все биты двоичного представления числа 127, кроме старшего, равны 1. В силу указанного свойства операции И операция ch & 127 оставляет все биты, кроме старшего, без изменения, а старший обнуляет: Бит контроля четности | V 1100 0001 переменная ch содержит символ 'A' с битом четности 0111 1111 двоичное представление числа 127 & --------- поразрядная операция И 0100 0001 символ 'A' с обнуленным битом контроля четности Поразрядная операция ИЛИ, являющаяся двойственной операции И, применяется для установки необходимых битов в 1. В следующем примере выполняется операция 128 | 3: | V 1000 0000 двоичное представление числа 128 0000 0011 двоичное представление числа 3 | --------- поразрядная операция ИЛИ 1000 0011 результат Операция исключающего ИЛИ (XOR) устанавливает бит результата в 1, если соответствующие биты операндов различны. В следующем примере выполняется операция 127 ^ 120: | V 0000 0011 двоичное представление числа 127 0111 1000 двоичное представление числа 120 ^ --------- поразрядная операция XOR 0000 0111 результат Необходимо помнить, что результат логической операции всегда равен 0 или 1. В то же время результатом поразрядной операции может быть любое значение, которое, как видно из предыдущих примеров, не обязательно равно 0 или 1. Поразрядные операторы сдвига >> и << сдвигают все биты переменной вправо или влево. Общая форма оператора сдвига вправо: переменная >> количество_разрядов Общая форма оператора сдвига влево: переменная << количество_разрядов Во время сдвига битов в один конец числа, другой конец заполняется нулями. Но если число типа signed int отрицательно, то при сдвиге вправо левый конец заполняется единицами, так что знак числа сохраняется. Необходимо отметить различие между сдвигом и циклическим сдвигом. При циклическом сдвиге биты, сдвигаемые за пределы операнда, появляются на другом конце операнда. А при сдвиге вышедшие за границу биты теряются. Поразрядные операции сдвига очень полезны при декодировании выходов внешних устройств, например таких, как цифро-аналоговые преобразователи, а также при считывании информации о статусе устройств. Побитовые операторы сдвига могут быстро умножать и делить целые числа. Сдвиг на один бит вправо делит число на 2, а на один бит влево — умножает на 2. Следующая программа иллюстрирует применение операторов сдвига: /* Пример применения операторов сдвига. */ #include int main(void) { unsigned int i; int j; i = 1; /* сдвиг влево */ for(j=0; j<4; j++) { i = i << 1; /* сдвиг i влево на 1 разраяд, что равносильно умножению на 2 */ printf("Сдвиг влево на %d разр.: %d\n", j, i); } /* сдвиг вправо */ for(j=0; j<4; j++) { i = i >> 1; /* сдвиг i вправо на 1 разраяд, что равносильно делению на 2 */ printf("Сдвиг вправо на %d разр.: %d\n", j, i); } return 0; } Каждый сдвиг влево умножает на 2. Потеря информации произошла после операции x << 2 в результате сдвига за левую границу. Каждый сдвиг вправо делит на 2. Сдвиг вправо потерянную информацию не восстановил. unsigned char x x после операции значение x x = 7 0000 0111 7 x = x << 1 0000 1110 14 x = x << 3 0111 0000 112 x = x << 2 1100 0000 192 x = x >> 1 0110 0000 96 x = x >> 2 0001 1000 24 Таблица Умножение и деление операторами сдвига Поразрядная операция отрицания (дополнения) ~ инвертирует состояние каждого бита операнда. То есть, 0 преобразует в 1, а 1 — в 0. Поразрядные операции часто используются в процедурах кодирования. Проделав с дисковым файлом некоторые поразрядные операции, его можно сделать нечитаемым. Простейший способ сделать это — применить операцию отрицания к каждому биту: Исходный байт 0010100 После 1-го отрицания 1101011 После 2-го отрицания 0010100 При последовательном применении 2-х отрицаний результатом всегда будет исходное число. Таким образом, 1-е отрицание кодирует состояние байта, а 2-е — декодирует. В следующем примере оператор отрицания используется в функции шифрования символа: /* Простейшая процедура шифрования. */ char encode(char ch) { return(~ch); /* оперция отрицания */ } Конечно, взломать такой шифр не представляет труда. Условная операция В языке С определен мощный и удобный оператор, который часто можно использовать вместо оператора вида if-then-else. Речь идет о тернарном операторе ?, общий вид которого следующий: Выражение1 ? Выражение2 : Выражение3; Оператор ? работает следующим образом: сначала вычисляется Выражение1, если оно истинно, то вычисляется Выражение2 и его значение присваивается всему выражению; если Выражение1 ложно, то вычисляется Выражение3 и всему выражению присваивается его значение. В примере x = 10; y = x>9 ? 100 : 200; переменной у будет присвоено значение 100. Если бы х было меньше 9, то переменной у было бы присвоено значение 200. Эту же процедуру можно написать, используя оператор if-else: x = 10; if(x>9) y = 100; else y = 200; Операция получения адреса (&) и раскрытия ссылки (*) Указатель — это адрес объекта в памяти. Переменная типа "указатель" (или просто переменная-указатель) — это специально объявленная переменная, в которой хранится указатель на переменную определенного типа. В языке С указатели служат мощнейшим средством создания программ и широко используются для самых разных целей. Например, с их помощью можно быстро обратиться к элементам массива или дать функции возможность модифицировать свои аргументы. Указатели широко используются для связи элементов в списках, в двоичных деревьях и в других динамических структурах данных. Оператор &, это унарный оператор, возвращающий адрес операнда в памяти[. (Унарной операцией называется операция, имеющая только один операнд.) Например, оператор m = &count; записывает в переменную m адрес переменной count. Этот адрес представляет собой адрес ячейки памяти компьютера, в которой размещена переменная. Адрес и значение переменной — совершенно разные понятия. Выражение "&переменная" означает "адрес переменной". Следовательно, инструкция m = &scount; означает: "Переменной m присвоить адрес, по которому расположена переменная count;". Допустим, переменная count расположена в памяти в ячейке с адресом 2000, а ее значение равно 100. Тогда в предыдущем примере переменной m будет присвоено значение 2000. Второй рассматриваемый оператор * является двойственным (дополняющим) по отношению к &. Оператор * является унарным оператором, он возвращает значение объекта, расположенного по указанному адресу. Операндом для * служит адрес объекта (переменной). Например, если переменная m содержит адрес переменной count, то оператор q = *m; записывает значение переменной count в переменную q. В нашем примере переменная q получит значение 100, потому что по адресу 2000 записано число 100, причем этот адрес записан в переменной m. Выражение "* адрес" означает "по адресу". Наш фрагмент программы можно прочесть как "q получает значение, расположенное по адресу m". К сожалению, символ операции раскрытия ссылки совпадает с символом операции умножения, а символ операции получения адреса — с символом операции поразрядного И. Необходимо помнить, что эти операторы не имеют никакого отношения друг к другу. Операторы * и & имеют более высокий приоритет, чем любая арифметическая операция, кроме унарного минуса, имеющего такой же приоритет. Если переменная является указателем, то в объявлении перед ее именем нужно поставить символ *, он сообщит компилятору о том, что это указатель на переменную данного типа. Например, объявление указателя на переменную типа char записывается так: char *ch; Необходимо понимать, что ch — это не переменная типа char, а указатель на переменную данного типа, это совершенно разные вещи. Тип данных, на который указывает указатель (в данном случае это char), называется базовым типом указателя. Сам указатель является переменной, содержащей адрес объекта базового типа. Компилятор учтет размер указателя в архитектуре компьютера и выделит для него необходимое количество байтов, чтобы в указатель поместился адрес. Базовый тип указателя определяет тип объекта, хранящегося по этому адресу. В одном операторе объявления можно одновременно объявить и указатель, и переменную, не являющуюся указателем. Например, оператор int x, *y, count; объявляет х и count как переменные целого типа, а у — как указатель на переменную целого типа. В следующей программе операторы * и & используются для записи значения 10 в переменную target. Программа выведет значение 10 на экран. #include int main(void) { int target, source; int *m; source = 10; m = &source; target = *m; printf("%d", target); return 0; } Операция определения размера sizof Унарная операция sizeof, выполняемая во время компиляции программы, позволяет определить длину операнда в байтах. Например, если компилятор для чисел типа int отводит 4 байта, а для чисел типа double — 8, то следующая программа напечатает 8 4. double f; printf("%d ", sizeof f); printf("%d", sizeof(int)); Необходимо помнить, что для вычисления размера типа переменной имя типа должно быть заключено в круглые скобки. Имя переменной заключать в скобки не обязательно, но ошибки в этом не будет. В языке С определяется (с помощью спецификатора класса памяти typedef) специальный тип size_t, приблизительно соответствующий целому числу без знака. Результат операции sizeof имеет тип size_t. Но практически его можно использовать везде, где допустимо использование целого числа без знака. Оператор sizeof очень полезен для улучшения переносимости программ, так как переносимость существенно зависит от размеров встроенных типов данных. Для примера рассмотрим программу, работающую с базой данных, в которой необходимо хранить шесть целых чисел в одной записи. Если эта программа предназначена для работы на многих компьютерах, ни в коем случае нельзя полагаться на то, что размер целого числа на всех компьютерах будет один и тот же. В программе следует определять размер целого, используя оператор sizeof. Соответствующая программа имеет следующий вид: /* Запись шести целых чисел в дисковый файл. */ void put_rec(int rec[6], FILE *fp) { int len; len = fwrite(rec, sizeof(int)*6, 1, fp); if(len != 1) printf("Ошибка при записи"); } Приведенная функция put_rec() компилируется и выполняется правильно в любой среде, в том числе на 16- и 32-разрядных компьютерах. И в заключение: оператор sizeof выполняется во время трансляции, его результат в программе рассматривается как константа. Оператор последовательного вычисления: оператор "запятая" Оператор "запятая" связывает воедино несколько выражений. При вычислении левой части оператора "запятая" всегда подразумевается, что она имеет тип void. Это значит, что выражение, стоящее справа после оператора "запятая", является значением всего разделенного запятыми выражения. Например, оператор x = (y=3, y+1); сначала присваивает у значение 3, а затем присваивает х значение 4. Скобки здесь обязательны, потому что приоритет оператора "запятая" меньший, чем оператора присваивания. В операторе "запятая" выполняется последовательность операций. Если этот оператор стоит в правой части оператора присваивания, то его результатом всегда является выражение, стоящее последним в списке. В языке Си принято следующее правило. Любое выражение с операцией присваивания, заключенное в круглые скобки, имеет значение, равное присваиваемому. Например, выражение (а=7+2) имеет значение 9. После этого можно записать другое выражение, например: ((а=7+2)<10), которое в данном случае будет всегда давать истинное значение. Следующая конструкция: ((сh = getch( )) == 'i') позволяет вводить значение переменной сh и давать истинный результат только тогда, когда введенным значением является буква 'i'. В скобках можно записывать и несколько формул, составляющих сложное выражение. Для этих целей используется операция запятая. Формулы будут вычисляться слева направо, и все выражение примет значение последней вычисленной формулы. Например, если имеются две переменные типа char, то выражение z = (х = у, у = getch( )); определяет следующие действия: значение переменной у присваивается переменной х; вводится символ с клавиатуры и присваивается переменной у; z получает значение переменной у. Скобки здесь необходимы, поскольку операция запятая имеет более низкий приоритет, чем операция присваивания, записанная после переменной z. Операция запятая находит широкое применение для построения выражений цикла for и позволяет параллельно изменять значения нескольких управляющих переменных. Оператор доступа к члену структуры (оператор . (точка)) и оператор доступа через указатель -> (оператор стрелка) В языке С операторы . (точка) и -> (стрелка) обеспечивают доступ к элементам структур и объединений. Структуры и объединения — это составные типы данных, в которых под одним именем хранятся многие объекты. Оператор точка используется для прямой ссылки на элемент структуры или объединения, т.е. перед точкой стоит имя структуры, а после — имя элемента структуры. Оператор стрелка используется с указателем на структуру или объединение, т.е. перед стрелкой стоит указатель на структуру. Например, во фрагменте программы struct employee { char name[80]; int age; float wage; } emp; struct employee *p = &emp; /* адрес emp заносится в p */ для присвоения члену wage значения 123.33 необходимо записать emp.wage = 123.23; То же самое можно сделать, использовав указатель на структуру: p->wage = 123.23; Оператор [] и () Круглые скобки являются оператором, повышающим приоритет выполнения операций, которые в них заключены. Квадратные скобки служат для индексации массива. Если в программе определен массив, то выражение в квадратных скобках представляет собой индекс массива. Например, в программе #include char s[80]; int main(void) { s[3] = 'X'; printf("%c", s[3]); return 0; } значение 'Х' сначала присваивается четвертому элементу массива (в С элементы массива нумеруются с нуля), затем этот элемент выводится на экран. Сводка приоритетов операций В таблице приведены приоритеты всех операций, определенных в С. Необходимо помнить, что все операторы, кроме унарных и "?", связывают (присоединяют, ассоциируют) свои операнды слева направо. Унарные операторы (*, &, -) и "?" связывают (присоединяют, ассоциируют) свои операнды справа налево. Знак операции Назначение операции Знак операции Назначение операции ( ) Вызов функции << Сдвиг влево [ ] Выделение элемента массива >> Сдвиг вправо . Выделение элемента записи < Меньше, чем -> Выделение элемента записи <= Меньше или равно ! Логическое отрицание > Больше, чем ~ Поразрядное отрицание >= Больше или равно - Изменение знака = = Равно ++ Увеличение на единицу != Не равно -- Уменьшение на единицу & Поразрядное логическое "И" & Взятие адреса ^ Поразрядное исключающее "ИЛИ" * Обращение по адресу | Поразрядное логическое "ИЛИ" (тип) Преобразование типа (т.е. (float) a) && Логическое "И" sizeof( ) Определение размера в байтах || Логическое "ИЛИ" * Умножение ?: Условная (тернарная) операция / Деление = Присваивание % Определение остатка от деления +=, - =, *=, /=, %=, <<=, >>=, &=, |=, ^= Составные операции присваивания (например, а *= b (т.е. a = a * b) и т.д.) + Сложение , Операция запятая - Вычитание Выражения Выражения состоят из операторов, констант, функций и переменных. В языке С выражением является любая правильная последовательность этих элементов. Большинство выражений в языке С по форме очень похожи на алгебраические, часто их и пишут, руководствуясь правилами алгебры. Однако здесь необходимо быть внимательным и учитывать специфику выражений в языке С. Порядок вычисления подвыражений в выражениях языка С не определен. Компилятор может самостоятельно перестроить выражение с целью создания оптимального объектного кода. Это значит, что программист не может полагаться на определенную последовательность вычисления подвыражений. Например, при вычислении выражения х = f1() + f2(); нет никаких гарантий того, что функция f1() будет вызвана перед вызовом f2(). Преобразования при вычислении выражений При выполнении операций производится автоматическое преобразование типов, чтобы привести операнды выражений к общему типу или чтобы расширить короткие величины до размера целых величин, используемых в машинных командах. Выполнение преобразования зависит от специфики операций и от типа операнда или операндов. Общие арифметические преобразования. 1. Операнды типа float преобразуются к типу double. 2. Если один операнд long double, то второй преобразуется к этому же типу. 3. Если один операнд double, то второй также преобразуется к типу double. 4. Любые операнды типа char и short преобразуются к типу int. 5. Любые операнды unsigned char или unsigned short преобразуются к типу unsigned int. 6. Если один операнд типа unsigned long, то второй преобразуется к типу unsigned long. 7. Если один операнд типа long, то второй преобразуется к типу long. 8. Если один операнд типа unsigned int, то второй операнд преобразуется к этому же типу. Таким образом, при вычислении выражений операнды преобразуются к типу того операнда, который имеет наибольший размер. Пример: double ft,sd; unsigned char ch; unsigned long in; int i; .... sd=ft*(i+ch/in); При выполнении оператора присваивания правила преобразования будут использоваться следующим образом. Операнд ch преобразуется к unsigned int (правило 5). Затем он преобразуется к типу unsigned long (правило 6). По этому же правилу i преобразуется к unsigned long и результат операции, заключенной в круглые скобки будет иметь тип unsigned long. Затем он преобразуется к типу double (правило 3) и результат всего выражения будет иметь тип double. Преобразования при вызове функции Преобразования, выполняемые над аргументами при вызове функции, зависят от того, был ли задан прототип функции (объявление "вперед") со списком объявлений типов аргументов. Если задан прототип функции и он включает объявление типов аргументов, то над аргументами в вызове функции выполняются только обычные арифметические преобразования. Эти преобразования выполняются независимо для каждого аргумента. Величины типа float преобразуются к double, величины типа char и short преобразуются к int, величины типов unsigned char и unsigned short преобразуются к unsigned int. Могут быть также выполнены неявные преобразования переменных типа указатель. Задавая прототипы функций, можно переопределить эти неявные преобразования и позволить компилятору выполнить контроль типов. Преобразования при приведении типов Явное преобразование типов может быть осуществлено посредством операции приведения типов, которая имеет формат: ( имя-типа ) операнд . В приведенной записи имя-типа задает тип, к которому должен быть преобразован операнд. Пример: int i=2; long l=2; double d; float f; d=(double)i * (double)l; f=(float)d; В данном примере величины i,l,d будут явно преобразовываться к указанным в круглых скобках типам. 9 Типы данных, определяемых пользователем: переименование типов, перечисления, структуры, объединения Переменные перечислимого типа Переменная, которая может принимать значение из некоторого списка значений, называется переменной перечислимого типа или перечислением. Объявление перечисления начинается с ключевого слова enum и имеет два формата представления. Формат 1. enum [имя-тега-перечисления] {список-перечисления} описатель[,описатель...]; Формат 2. enum имя-тега-перечисления описатель [,описатель..]; Объявление перечисления задает тип переменной перечисления и определяет список именованных констант, называемый списком-перечисления. Значением каждого имени списка является некоторое целое число. Переменная типа перечисления может принимать значения одной из именованных констант списка. Именованные константы списка имеют тип int. Таким образом, память соответствующая переменной перечисления, это память необходимая для размещения значения типа int. Переменная типа enum могут использоваться в индексных выражениях и как операнды в арифметических операциях и в операциях отношения. В первом формате 1 имена и значения перечисления задаются в списке перечислений. Необязательное имя-тега-перечисления, это идентификатор, который именует тег перечисления, определенный списком перечисления. Описатель именует переменную перечисления. В объявлении может быть задана более чем одна переменная типа перечисления. Список-перечисления содержит одну или несколько конструкций вида: идентификатор [= константное выражение] Каждый идентификатор именует элемент перечисления. Все идентификаторы в списке enum должны быть уникальными. В случае отсутствия константного выражения первому идентификатору соответствует значение 0, следующему идентификатору - значение 1 и т.д. Имя константы перечисления эквивалентно ее значению. Идентификатор, связанный с константным выражением, принимает значение, задаваемое этим константным выражением. Константное выражение должно иметь тип int и может быть как положительным, так и отрицательным. Следующему идентификатору в списке присваивается значение, равное константному выражению плюс 1, если этот идентификатор не имеет своего константного выражения. Использование элементов перечисления должно подчиняться следующим правилам: 1. переменная может содержать повторяющиеся значения. 2. идентификаторы в списке перечисления должны быть отличны от всех других идентификаторов в той же области видимости, включая имена обычных переменных и идентификаторы из других списков перечислений. 3. имена типов перечислений должны быть отличны от других имен типов перечислений, структур и смесей в этой же области видимости. 4. значение может следовать за последним элементом списка перечисления. Пример: enum week { SUB = 0, /* 0 */ VOS = 0, /* 0 */ POND, /* 1 */ VTOR, /* 2 */ SRED, /* 3 */ HETV, /* 4 */ PJAT /* 5 */ } rab_ned ; В данном примере объявлен перечислимый тег week, с соответствующим множеством значений, и объявлена переменная rab_ned имеющая тип week. Во втором формате используется имя тега перечисления для ссылки на тип перечисления, определяемый где-то в другом месте. Имя тега перечисления должно относится к уже определенному тегу перечисления в пределах текущей области видимости. Так как тег перечисления объявлен где-то в другом месте, список перечисления не представлен в объявлении. Пример: enum week rab1; В объявлении указателя на тип данных перечисления и объявляемых typedef для типов перечисления можно использовать имя тега перечисления до того, как данный тег перечисления определен. Однако определение перечисления должно предшествовать любому действию используемого указателя на тип объявления typedef. Объявление без последующего списка описателей описывает тег, или, если так можно сказать, шаблон перечисления. Структуры Cтруктуры - это составной объект, в который входят элементы любых типов, за исключением функций. В отличие от массива, который является однородным объектом, структура может быть неоднородной. Тип структуры определяется записью вида: struct { список определений } В структуре обязательно должен быть указан хотя бы один компонент. Определение структур имеет следующий вид: тип-данных описатель; где тип-данных указывает тип структуры для объектов, определяемых в описателях. В простейшей форме описатели представляют собой идентификаторы или массивы. Пример: struct { double x,y; } s1, s2, sm[9]; struct { int year; char moth, day; } date1, date2; Переменные s1, s2 определяются как структуры, каждая из которых состоит из двух компонент х и у. Переменная sm определяется как массив из девяти структур. Каждая из двух переменных date1, date2 состоит из трех компонентов year, moth, day. >p>Существует и другой способ ассоциирования имени с типом структуры, он основан на использовании тега структуры. Тег структуры аналогичен тегу перечислимого типа. Тег структуры определяется следующим образом: struct тег { список описаний; }; где тег является идентификатором. В приведенном ниже примере идентификатор student описывается как тег структуры: struct student { char name[25]; int id, age; char prp; }; Тег структуры используется для последующего объявления структур данного вида в форме: struct тег список-идентификаторов; Пример: struct studeut st1,st2; Использование тегов структуры необходимо для описания рекурсивных структур. Ниже рассматривается использование рекурсивных тегов структуры. struct node { int data; struct node * next; } st1_node; Тег структуры node действительно является рекурсивным, так как он используется в своем собственном описании, т.е. в формализации указателя next. Структуры не могут быть прямо рекурсивными, т.е. структура node не может содержать компоненту, являющуюся структурой node, но любая структура может иметь компоненту, являющуюся указателем на свой тип, как и сделано в приведенном примере. Доступ к компонентам структуры осуществляется с помощью указания имени структуры и следующего через точку имени выделенного компонента, например: st1.name="Иванов"; st2.id=st1.id; st1_node.data=st1.age; Объединения Объединение подобно структуре, однако в каждый момент времени может использоваться (или другими словами быть ответным) только один из элементов объединения. Тип объединения может задаваться в следующем виде: union { описание элемента 1; ... описание элемента n; }; Главной особенностью объединения является то, что для каждого из объявленных элементов выделяется одна и та же область памяти, т.е. они перекрываются. Хотя доступ к этой области памяти возможен с использованием любого из элементов, элемент для этой цели должен выбираться так, чтобы полученный результат не был бессмысленным. Доступ к элементам объединения осуществляется тем же способом, что и к структурам. Тег объединения может быть формализован точно так же, как и тег структуры. Объединение применяется для следующих целей: - инициализации используемого объекта памяти, если в каждый момент времени только один объект из многих является активным; - интерпретации основного представления объекта одного типа, как если бы этому объекту был присвоен другой тип. Память, которая соответствует переменной типа объединения, определяется величиной, необходимой для размещения наиболее длинного элемента объединения. Когда используется элемент меньшей длины, то переменная типа объединения может содержать неиспользуемую память. Все элементы объединения хранятся в одной и той же области памяти, начиная с одного адреса. Пример: union { char fio[30]; char adres[80]; int vozrast; int telefon; } inform; union { int ax; char al[2]; } ua; При использовании объекта infor типа union можно обрабатывать только тот элемент который получил значение, т.е. после присвоения значения элементу inform.fio, не имеет смысла обращаться к другим элементам. Объединение ua позволяет получить отдельный доступ к младшему ua.al[0] и к старшему ua.al[1] байтам двухбайтного числа ua.ax . Поля битов Элементом структуры может быть битовое поле, обеспечивающее доступ к отдельным битам памяти. Вне структур битовые поля объявлять нельзя. Нельзя также организовывать массивы битовых полей и нельзя применять к полям операцию определения адреса. В общем случае тип структуры с битовым полем задается в следующем виде: struct { unsigned идентификатор 1 : длина-поля 1; unsigned идентификатор 2 : длина-поля 2; } длинна - поля задается целым выражением или константой. Эта константа определяет число битов, отведенное соответствующему полю. Поле нулевой длинны обозначает выравнивание на границу следующего слова. Пример: struct { unsigned a1 : 1; unsigned a2 : 2; unsigned a3 : 5; unsigned a4 : 2; } prim; Структуры битовых полей могут содержать и знаковые компоненты. Такие компоненты автоматически размещаются на соответствующих границах слов, при этом некоторые биты слов могут оставаться неиспользованными. Ссылки на поле битов выполняются точно так же, как и компоненты общих структур. Само же битовое поле рассматривается как целое число, максимальное значение которого определяется длиной поля. Переменные с изменяемой структурой Очень часто некоторые объекты программы относятся к одному и тому же классу, отличаясь лишь некоторыми деталями. Рассмотрим, например, представление геометрических фигур. Общая информация о фигурах может включать такие элементы, как площадь, периметр. Однако соответствующая информация о геометрических размерах может оказаться различной в зависимости от их формы. Рассмотрим пример, в котором информация о геометрических фигурах представляется на основе комбинированного использования структуры и объединения. struct figure { double area,perimetr; /* общие компоненты */ int type; /* признак компонента */ union /* перечисление компонент */ { double radius; /* окружность */ double a[2]; /* прямоугольник */ double b[3]; /* треугольник */ } geom_fig; } fig1, fig2 ; В общем случае каждый объект типа figure будет состоять из трех компонентов: area, perimetr, type. Компонент type называется меткой активного компонента, так как он используется для указания, какой из компонентов объединения geom_fig является активным в данный момент. Такая структура называется переменной структурой, потому что ее компоненты меняются в зависимости от значения метки активного компонента (значение type). Вместо компоненты type типа int, целесообразно было бы использовать перечисляемый тип. Например, такой enum figure_chess { CIRCLE, BOX, TRIANGLE } ; Константы CIRCLE, BOX, TRIANGLE получат значения соответственно равные 0, 1, 2. Переменная type может быть объявлена как имеющая перечислимый тип : enum figure_chess type; В этом случае компилятор СИ предупредит программиста о потенциально ошибочных присвоениях, таких, например, как figure.type = 40; В общем случае переменная структуры будет состоять из трех частей: набор общих компонент, метки активного компонента и части с меняющимися компонентами. Общая форма переменной структуры, имеет следующий вид: struct { общие компоненты; метка активного компонента; union { описание компоненты 1 ; описание компоненты 2 ; ::: описание компоненты n ; } идентификатор-объединения ; } идентификатор-структуры ; Пример определения переменной структуры с именем helth_record struct { /* общая информация */ char name [25]; /* имя */ int age; /* возраст */ char sex; /* пол */ /* метка активного компонента */ /* (семейное положение) */ enum merital_status ins; /* переменная часть */ union { /* холост */ /* нет компонент */ struct { /* состоит в браке */ char marripge_date[8]; char spouse_name[25]; int no_children; } marriage_info; /* разведен */ char date_divorced[8]; } marital_info; } health_record; enum marital_status { SINGLE, /* холост */ MARRIGO, /* женат */ DIVOREED /* разведен */ } ; Обращаться к компонентам структуры можно при помощи ссылок: helth_record.neme, helth_record.ins, helth_record.marriage_info.marriage_date . Определение объектов и типов Все переменные используемые в программах на языке СИ, должны быть объявлены. Тип объявляемой переменной зависит от того, какое ключевое слово используется в качестве спецификатора типа и является ли описатель простым идентификатором или же комбинацией идентификатора с модификатором указателя (звездочка), массива (квадратные скобки) или функции (круглые скобки). При объявлении простой переменной, структуры, смеси или объединения, а также перечисления, описатель - это простой идентификатор. Для объявления указателя, массива или функции идентификатор модифицируется соответствующим образом: звездочкой слева, квадратными или круглыми скобками справа. При объявлении можно использовать одновременно более одного модификатора, что дает возможность создавать множество различных сложных описателей типов. Однако надо помнить, что некоторые комбинации модификаторов недопустимы: - элементами массивов не могут быть функции, - функции не могут возвращать массивы или функции. При инициализации сложных описателей квадратные и круглые скобки (справа от идентификатора) имеют приоритет перед звездочкой (слева от идентификатора). Квадратные или круглые скобки имеют один и тот же приоритет и раскрываются слева направо. Спецификатор типа рассматривается на последнем шаге, когда описатель уже полностью проинтерпретирован. Можно использовать круглые скобки, чтобы поменять порядок интерпретации на необходимый. Для интерпретации сложных описаний предлагается простое правило, которое звучит как "изнутри наружу", и состоит из четырех шагов: 1. начать с идентификатора и посмотреть вправо, есть ли квадратные или круглые скобки. 2. если они есть, то проинтерпретировать эту часть описателя и затем посмотреть налево в поиске звездочки. 3. если на любой стадии справа встретится закрывающая круглая скобка, то вначале необходимо применить все эти правила внутри круглых скобок, а затем продолжить интерпретацию. 4. интерпретировать спецификатор типа. Примеры: int * ( * comp [10]) (); 6 5 3 1 2 4 В данном примере объявляется переменная comp (1), как массив из десяти (2) указателей (3) на функции (4), возвращающие указатели (5) на целые значения (6). char * ( * ( * var ) () ) [10]; 7 6 4 2 1 3 5 Переменная var (1) объявлена как указатель (2) на функцию (3) возвращающую указатель (4) на массив (5) из 10 элементов, которые являются указателями (6) на значения типа char. Кроме объявлений переменных различных типов, имеется возможность объявить типы. Это можно сделать двумя способами. Первый способ - указать имя тега при объявлении структуры, объединения или перечисления, а затем использовать это имя в объявлении переменных и функций в качестве ссылки на этот тег. Второй - использовать для объявления типа ключевое слово typedef. При объявлении с ключевым словом typedef, идентификатор стоящий на месте описываемого объекта, является именем вводимого в рассмотрение типа данных, и далее этот тип может быть использован для объявления переменных. Любой тип может быть объявлен с использованием ключевого слова typedef, включая типы указателя, функции или массива. Имя с ключевым словом typedef для типов указателя, структуры, объединения может быть объявлено прежде чем эти типы будут определенны, но в пределах видимости объявителя. Примеры: typedef double (* MATH)( ); /* MATH - новое имя типа, представляющее указатель на функцию, возвращающую значения типа double */ MATH cos; /* cos указатель на функцию, возвращающую значения типа double */ /* Можно провести эквивалентное объявление */ double (* cos)( ); typedef char FIO[40] /* FIO - массив из сорока символов */ FIO person; /* Переменная person - массив из сорока символов */ /* Это эквивалентно объявлению */ char person[40]; При объявлении переменных и типов здесь были использованы имена типов (MATH FIO). Помимо этого, имена типов могут еще использоваться в трех случаях: в списке формальных параметров, в объявлении функций, в операциях приведения типов и в операции sizeof (операция приведения типа). Именами типов для основных типов, типов перечисления, структуры и смеси являются спецификаторы типов для этих типов. Имена типов для типов указателя массива и функции задаются при помощи абстрактных описателей следующим образом: спецификатор-типа абстрактный-описатель; Абстрактный-описатель - это описатель без идентификатора, состоящий из одного или более модификаторов указателя, массива или функции. Модификатор указателя (*) всегда задается перед идентификатором в описателе, а модификаторы массива [] и функции () - после него. Таким образом, чтобы правильно интерпретировать абстрактный описатель, нужно начать интерпретацию с подразумеваемого идентификатора. Абстрактные описатели могут быть сложными. Скобки в сложных абстрактных описателе задают порядок интерпретации подобно тому, как это делалось при интерпретации сложных описателей в объявлениях. Инициализация данных При объявлении переменной ей можно присвоить начальное значение, присоединяя инициатор к описателю. Инициатор начинается со знака "=" и имеет следующие формы. Формат 1: = инициатор; Формат 2: = { список - инициаторов }; Формат 1 используется при инициализации переменных основных типов и указателей, а формат 2 - при инициализации составных объектов. Примеры: char tol = 'N'; Переменная tol инициализируется символом 'N'. const long megabute = (1024 * 1024); Немодифицируемая переменная megabute инициализируется константным выражением после чего она не может быть изменена. static int b[2][2] = {1,2,3,4}; Инициализируется двухмерный массив b целых величин элементам массива присваиваются значения из списка. Эта же инициализация может быть выполнена следующим образом : static int b[2][2] = { { 1,2 }, { 3,4 } }; При инициализации массива можно опустить одну или несколько размерностей static int b[3[] = { { 1,2 }, { 3,4 } }; Если при инициализации указано меньше значений для строк, то оставшиеся элементы инициализируются 0, т.е. при описании static int b[2][2] = { { 1,2 }, { 3 } }; элементы первой строки получат значения 1 и 2, а второй 3 и 0. При инициализации составных объектов, нужно внимательно следить за использованием скобок и списков инициализаторов. Примеры: struct complex { double real; double imag; } comp [2][3] = { { {1,1}, {2,3}, {4,5} }, { {6,7}, {8,9}, {10,11} } }; В данном примере инициализируется массив структур comp из двух строк и трех столбцов, где каждая структура состоит из двух элементов real и imag. struct complex comp2 [2][3] = { {1,1},{2,3},{4,5}, {6,7},{8,9},{10,11} }; В этом примере компилятор интерпретирует рассматриваемые фигурные скобки следующим образом: - первая левая фигурная скобка - начало составного инициатора для массива comp2; - вторая левая фигурная скобка - начало инициализации первой строки массива comp2[0]. Значения 1,1 присваиваются двум элементам первой структуры; - первая правая скобка (после 1) указывает компилятору, что список инициаторов для строки массива окончен, и элементы оставшихся структур в строке comp[0] автоматически инициализируются нулем; - аналогично список {2,3} инициализирует первую структуру в строке comp[1], а оставшиеся структуры массива обращаются в нули; - на следующий список инициализаторов {4,5} компилятор будет сообщать о возможной ошибке так как строка 3 в массиве comp2 отсутствует. При инициализации объединения задается значение первого элемента объединения в соответствии с его типом. Пример: union tab { unsigned char name[10]; int tab1; } pers = {'A','H','T','O','H'}; Инициализируется переменная pers.name, и так как это массив, для его инициализации требуется список значений в фигурных скобках. Первые пять элементов массива инициализируются значениями из списка, остальные нулями. Инициализацию массива символов можно выполнить путем использования строкового литерала. char stroka[ ] = "привет"; Инициализируется массив символов из 7 элементов, последним элементом (седьмым) будет символ '\0', которым завершаются все строковые литералы. В том случае, если задается размер массива, а строковый литерал длиннее, чем размер массива, то лишние символы отбрасываются. Следующее объявление инициализирует переменную stroka как массив, состоящий из семи элементов. char stroka[5] = "привет"; В переменную stroka попадают первые пять элементов литерала, а символы 'Т' и '\0' отбрасываются. Если строка короче, чем размер массива, то оставшиеся элементы массива заполняются нулями. Инициализация переменной типа tab может иметь следующий вид: union tab pers1 = "Антон"; и, таким образом, в символьный массив попадут символы: 'А','Н','Т','О','Н','\0', а остальные элементы будут инициализированы нулем. 10 Структура программы на Си. Разработка программ линейной структуры 2 Структура программы на языке С В табл. 1.2 перечислены 32 ключевых слова, определенные стандартом С89. Они же являются ключевыми словами языка С как подмножества C++. В табл. 1.3 приведены ключевые слова, добавленные стандартом С99. Набор ключевых слов вместе с формальным синтаксисом С составляет язык программирования С. Ключевые слова стандарта C89 auto double int struct break else long switch case enum register typedef char extern return union const float short unsigned continue for signed void default goto sizof volatile do if static while Ключевые слова, добавленные стандартом C99 _Bool _Imaginary restrict _Complex inline Кроме стандартных ключевых слов, многие компиляторы для лучшего функционирования в среде программирования разрешают дополнительно использовать некоторые нестандартные ключевые слова. Например, несколько компиляторов, рассчитанных на создание кода, выполняемого в моделях памяти, поддерживаемых процессорами семейства 8086, с целью поддержки взаимодействия программ, написанных на разных языках, а также для обеспечения доступа к прерываниям дополнительно вводят следующие ключевые слова: Asm _ds huge pascal Cdecl _es interrupt _ss _cs far near Для наиболее эффективного использования возможностей конкретного компилятора программист обязательно должен ознакомиться с набором дополнительных ключевых слов. В языке С различаются верхний и нижний регистры символов: else — ключевое слово, a ELSE — нет. В программе ключевое слово может быть использовано только как ключевое слово, то есть никогда не допускается его использование в качестве переменной или имени функции. Любая программа на С состоит из одной или нескольких функций. Обязательно должна быть определена единственная главная функция main(), именно с нее всегда начинается выполнение программы. В хорошем исходном тексте программы главная функция всегда содержит операторы, отражающие сущность решаемой задачи, чаще всего это вызовы функций. Хотя main() и не является ключевым словом, относиться к нему следует как к ключевому. Например, не следует использовать main как имя переменной, так как это может нарушить работу транслятора. Структура программы С, здесь f1() — fN() означают функции, написанные программистом. Объявление глобальных переменных int main(список параметров) { последовательность операторов } тип_возвращаемого_значения f1(список параметров) { последовательность операторов } тип_возвращаемого_значения f2(список параметров) { последовательность операторов } тип_возвращаемого_значения fN(список параметров) { последовательность операторов } Библиотека и компановка На С в принципе возможно создать программу, содержащую только имена переменных и ключевые слова. Но обычно так не поступают, потому что в С нет ключевых слов для выполнения многих операций, например, таких как ввод/вывод, вычисление математических функций, обработка строк и т.п. Поэтому в большинстве программ присутствуют вызовы различных функций, хранящихся в библиотеке стандартных функций С. Все компиляторы С поставляются вместе с библиотекой стандартных функций, предназначенных для выполнения наиболее общих задач. Стандарт С определяет минимальный набор функций, которые должны поддерживаться каждым компилятором. Но обычно библиотеки, поставляемые с компиляторами, имеют и много других, дополнительных, функций. Например, в стандартной библиотеке нет функций для работы с графикой, зато они есть почти в каждом компиляторе. При вызове библиотечной функции компилятор "запоминает" ее имя. Потом компоновщик связывает код исходной программы с объектным кодом, уже найденным в стандартной библиотеке. Этот процесс называется компоновкой или редактированием связей. У некоторых компиляторов есть свой собственный компоновщик, другие пользуются стандартным компоновщиком, поставляемым вместе с операционной системой. В библиотеке функции хранятся в переместимом формате. Это значит, что адреса машинных инструкций в памяти являются не абсолютными, а относительными. При компоновке программы с функциями из стандартной библиотеки эти относительные адреса, или смещения, используются для определения действительных адресов. Библиотека стандартных функций содержит большое количество функций, необходимых для написания программы. Кроме того, программист может написать свою функцию и поместить ее в библиотеку. Раздельная компиляция Короткая программа на языке С может состоять всего лишь из одного файла исходного текста. Однако при увеличении длины программы увеличивается также и время . Программа на С может состоять из двух или более файлов, компилируемых отдельно. Скомпилированные файлы программы компонуются с процедурами из библиотеки, формируя, таким образом, объектный код программы. Преимущество раздельной компиляции состоит в том, что при изменении одного файла нет необходимости перекомпилировать заново всю программу. При работе со сложными проектами это экономит много времени. Раздельная компиляция позволяет также нескольким программистам работать над одним проектом, так как она служит средством организации исходного текста программы для большого проекта. Компиляция программы на языке С Создание выполнимой программы на языке С состоит из следующих трех шагов: 1. разработка, 2. компиляция 3. и компоновка программы с библиотечными функциями. В настоящее время большинство компиляторов поставляется вместе с оболочкой программирования, содержащей редактор текста. Оболочки содержат обычно также автономный компилятор. При наличии автономного компилятора для написания программы можно использовать любой удобный редактор. В противном случае нужно быть очень внимательным, так как встроенный компилятор нормально работает только со стандартным текстовым файлом. Например, компиляторы не могут обрабатывать файлы, созданные некоторыми текстовыми процессорами, так как эти файлы содержат управляющие коды и непечатаемые символы. Конкретный способ компиляции программы зависит от типа используемого компилятора. Для разных компиляторов и оболочек способы компоновки также могут быть разными, например, компоновка может выполняться компилятором, а может и отдельной программой. Эти вопросы обычно освещаются в документации компилятора. Карта памяти программы на языке С Скомпилированная программа С имеет четыре логически обособленные области памяти. Первая — это область памяти, содержащая выполнимый код программы. Во второй области хранятся глобальные переменные. Оставшиеся две области — это стек и динамически распределяемая область памяти. Стек используется для хранения вспомогательных переменных во время выполнения программы. Здесь находятся адреса возврата функций, аргументы функций, локальные переменные и т.п. Текущее состояние процессора также хранится в стеке. Динамически распределяемая область памяти, или куча — это такая свободная область памяти, для получения участков памяти из которой программа вызывает функции динамического распределения памяти. На рисунке показано, как распределяется память во время выполнения программы. Конкретное распределение может быть разным в зависимости от типа процессора и реализации языка. Распределение памяти (карта памяти) при выполнении программ, написанной на языке C Стек ↓ Динамически распределяемая область памяти ↓ Глобальные переменные ↓ Код программы 11 Управляющая структура Ветвление. Полное, неполное Ветвление, Выбор. Правила организации и тестирование разветвленных алгоритмов Операторы Оператор — это часть программы, которая может быть выполнена отдельно. Оператор определяет некоторое действие. В языке С существуют следующие группы операторов: • пустой оператор; • составной оператор; • оператор – выражение; • условные операторы • операторы цикла • операторы безусловного перехода • метки • операторы-выражения • блоки К условным относятся операторы if и switch. Иногда их также называют операторами условного перехода. Операторы цикла — это while, for и do-while. К операторам безусловного перехода относятся break, continue, goto и return. К меткам относятся операторы case, default и собственно метки. Операторы-выражения — это операторы, состоящие из допустимых выражений. Блок представляет собой фрагмент текста программы, обрамленный фигурными скобками {}. Блок иногда называют составным оператором. Все операторы языка СИ, кроме составных операторов, заканчиваются точкой с запятой ";". Пустой оператор Пустой оператор состоит только из точки с запятой. При выполнении этого оператора ничего не происходит. Обычно используется в следующих случаях: - в операторах do, for, while, if в строках, когда по синтаксису требуется хотя бы один оператор, но не возникает необходимости выполнять что-либо; - при необходимости пометить фигурную скобку. Синтаксис языка СИ требует, чтобы после метки обязательно следовал оператор. Фигурная же скобка оператором не является. Поэтому, если надо передать управление на фигурную скобку, необходимо использовать пустой оператор. Пример: int main ( ) { : { if (...) goto a; /* переход на скобку */ { ... } a:; } return 0; } По принципам структурного программирования использование оператора безусловного перехода не рекомендуется. Составной оператор (блок) Составной оператор представляет собой несколько операторов и объявлений, заключенных в фигурные скобки: { [oбъявление] : оператор; [оператор]; : } В конце составного оператора точка с запятой не ставится. Эта последовательность операторов рассматривается как одна программная единица. Операторы, составляющие блок, логически связаны друг с другом. Чаще всего блок используется как составная часть какого-либо оператора, выполняющего действие над группой операторов, например, if или for. Однако блок можно поставить в любом месте, где может находиться оператор. Выполнение составного оператора заключается в последовательном выполнении составляющих его операторов. Пример: int main () { int q,b; double t,d; : if (...) { int e,g; double f,q; : } : return (0); } Переменные e,g,f,q будут недоступны после выполнения составного оператора. Переменная q является локальной в составном операторе, т.е. она никак не связана с переменной q объявленной в начале функции main с типом int. Пример: #include int main(void) { int i; { /* блок операторов */ i = 120; printf("%d", i); } return 0; } Оператор выражение Любое выражение, которое заканчивается точкой с запятой, является оператором. Выполнение оператора выражение заключается в вычислении выражения. Полученное значение выражения никак не используется, поэтому, как правило, такие выражения вызывают побочные эффекты. Вызвать функцию, не возвращающую значения можно только при помощи оператора выражения. Примеры: ++ i; Этот оператор представляет выражение, которое увеличивает значение переменной i на единицу. a=cos(b * 5); Этот оператор представляет выражение, включающее в себя операции присваивания и вызова функции. a(x,y); Этот оператор представляет выражение, состоящее из вызова функции. Выражение стоящее после return может быть заключено в круглые скобки, хотя их наличие необязательно. Операторы безусловного перехода Оператор break Оператор break обеспечивает прекращение выполнения самого внутреннего из объединяющих его операторов switch, do, for, while. После выполнения оператора break управление передается оператору, следующему за прерванным. Оператор continue Оператор continue, как и оператор break, используется только внутри операторов цикла, но в отличие от него выполнение программы продолжается не с оператора, следующего за прерванным оператором, а с начала прерванного оператора. Формат оператора следующий: continue; Пример: int main() { int a,b; for (a=1,b=0; a<100; b+="a,a++)" { if (b%2) continue; ... /* обработка четных сумм */ } return 0; } Когда сумма чисел от 1 до а становится нечетной, оператор continue передает управление на очередную итерацию цикла for, не выполняя оператор обработки четных сумм. Оператор continue, как и оператор break, прерывает самый внутренний из объемлющих его циклов. Оператор return Оператор return завершает выполнение функции, в которой он задан, и возвращает управление в вызывающую функцию, в точку, непосредственно следующую за вызовом. Функция main передает управление операционной системе. Формат оператора: return [выражение] ; Значение выражения, если оно задано, возвращается в вызывающую функцию в качестве значения вызываемой функции. Если выражение опущено, то возвращаемое значение не определено. Выражение может быть заключено в круглые скобки, хотя их наличие не обязательно. Если в какой-либо функции отсутствует оператор return, то передача управления в вызывающую функцию происходит после выполнения последнего оператора вызываемой функции. При этом возвращаемое значение не определено. Если функция не должна иметь возвращаемого значения, то ее нужно объявлять с типом void. Таким образом, использование оператора return необходимо либо для немедленного выхода из функции, либо для передачи возвращаемого значения. Пример: int sum (int a, int b) { renurn (a+b); } Функция sum имеет два формальных параметра a и b типа int, и возвращает значение типа int, о чем говорит описатель, стоящий перед именем функции. Возвращаемое оператором return значение равно сумме фактических параметров. Пример: void prov (int a, double b) { double c; if (a<3) return; else if (b>10) return; else { c=a+b; if ((2*c-b)==11) return; } } В этом примере оператор return используется для выхода из функции в случае выполнения одного из проверяемых условий. Оператор goto !!!Использование оператора безусловного перехода goto в практике программирования на языке СИ настоятельно не рекомендуется, так как он затрудняет понимание программ и возможность их модификаций!!!! Формат этого оператора следующий: goto имя-метки; ... имя-метки: оператор; Оператор goto передает управление на оператор, помеченный меткой имя-метки. Помеченный оператор должен находиться в той же функции, что и оператор goto, а используемая метка должна быть уникальной, т.е. одно имя-метки не может быть использовано для разных операторов программы. Имя-метки - это идентификатор. Любой оператор в составном операторе может иметь свою метку. Используя оператор goto, можно передавать управление внутрь составного оператора. Но нужно быть осторожным при входе в составной оператор, содержащий объявления переменных с инициализацией, так как объявления располагаются перед выполняемыми операторами и значения объявленных переменных при таком переходе будут не определены. Условные операторы. Правила организации разветвлений В языке С существуют два условных оператора: if и switch. При определенных обстоятельствах условная операция является альтернативой оператора if. Управляющая структура Ветвление. Оператор if Формат оператора: if (выражение) оператор1; [else оператор2;] где операто-1, оператор2 – любой оператор С, в том числе составной. Работает оператор полного Ветвления следующим образом: 1. Сначала вычисляется выражение; 2. если значение выражение истинно (т.е. отлично от 0), то выполняется оператор1; 3. если выражение ложно (т.е. равно 0),то выполняется оператор2. В случае если оператор2 отсутствует, Ветвление называется неполным. После выполнения оператора if управление передается на следующий оператор программы. Условное выражение, входящее в if, должно иметь скалярный результат. Это значит, что результатом должно быть целое число, символ, указатель или число с плавающей точкой, но им не может быть массив или структура. (В Стандарте С99 тип _Вооl также является скалярным, поэтому значение этого типа может использоваться в условии оператора if.) В выражении результат плавающего типа используется редко, потому что это существенно замедляет вычислительный процесс. Пример: if (i < j) i++; else { j = i-3; i++; } Допускается использование вложенных операторов if. Оператор if может быть включен в конструкцию if или в конструкцию else другого оператора if. Чтобы сделать программу более читабельной, рекомендуется группировать операторы и конструкции во вложенных операторах if, используя фигурные скобки. Если же фигурные скобки опущены, то компилятор связывает каждое ключевое слово else с наиболее близким if, для которого нет else. Примеры: int main ( ) { int t=2, b=7, r=3; if (t>b) { if (b < r) r=b; } else r=t; return (0); } В результате выполнения этой программы r станет равным 2. Если же в программе опустить фигурные скобки, стоящие после оператора if, то программа будет иметь следующий вид: int main ( ) { int t=2,b=7,r=3; if ( a>b ) if ( b < c ) t=b; else r=t; return (0); } В этом случае r получит значение равное 3, так как ключевое слово else относится ко второму оператору if, который не выполняется, поскольку не выполняется условие, проверяемое в первом операторе if. Следующий фрагмент иллюстрирует вложенные операторы if: char ZNAC; int x,y,z; : if (ZNAC == '-') x = y - z; else if (ZNAC == '+') x = y + z; else if (ZNAC == '*') x = y * z; else if (ZNAC == '/') x = y / z; else ... В следующей программе иллюстрируется использование оператора if. В ней запрограммирована очень простая игра "угадай магическое число". Если играющий угадал число, на экран выводится сообщение **Верно**. Программа генерирует "магическое число" с помощью стандартного генератора случайных чисел rand(). Генератор возвращает случайное число в диапазоне между 0 и RAND_MAX (обычно это число не меньше 32767). Функция rand() объявлена в заголовочном файле . /* Магическое число*/ #include #include int main(void) { int magic; /* магическое число */ int guess; /* попытка игрока */ magic = rand(); /* генерация магического числа */ printf("Угадай магическое число: "); scanf("%d", &guess); if(guess == magic) printf("** Верно **"); return 0; } В следующей версии программы для игры в "магическое число" иллюстрируется использование оператора else. В этой версии выводится дополнительное сообщение в случае ложного ответа. /* Магическое число2. */ #include #include int main(void) { int magic; /* магическое число */ int guess; /* попытка игрока */ magic = rand(); /* генерация магического числа */ printf("Угадай магическое число: "); scanf("%d", &guess); if(guess == magic) printf("** Верно **"); else printf("Неверно"); return 0; } Вложенные условные операторы if Оператор if является вложенным, если он вложен, т.е. находится внутри другого оператора if или else. В практике программирования вложенные условные операторы используются довольно часто. Во вложенном условном операторе фраза else всегда ассоциирована с ближайшим if в том же блоке, если этот if не ассоциирован с другой фразой else. Например: if(i) { if(j) statement 1; if(k) statement 2; /* этот if */ else statement 3; /* ассоциирован с этим else */ } else statement 4; /* ассоциирован с if(i) */ Последняя фраза else не ассоциирована с if(j) потому, что она находится в другом блоке. Эта фраза else ассоциирована с if(i). Внутренняя фраза else ассоциирована с if(k), потому что этот if — ближайший. Стандарт С89 допускает 15 уровней вложенности условных операторов, С99 — 127 уровней. В настоящее время большинство компиляторов допускают значительно большее количество уровней вложенности. Однако на практике необходимость в глубине вложенности, большей, чем несколько уровней, возникает довольно редко, так как увеличение глубины вложенности быстро запутывает программу и делает ее нечитаемой. Оператор выбора - switch Оператор switch предназначен для организации выбора из множества различных вариантов. Формат оператора следующий: switch ( выражение ) { [объявление] : [ case константное-выражение1]: [ список-операторов1] [ case константное-выражение2]: [ список-операторов2] : : [ default: [ список операторов ]] } Выражение, следующее за ключевым словом switch в круглых скобках, может быть любым выражением, допустимыми в языке СИ, значение которого должно быть целым. Можно использовать явное приведение к целому типу, однако при этом необходимо помнить о возможной потере информации. Значение выражения является ключевым для выбора из нескольких вариантов. Тело оператора smitch состоит из нескольких операторов, помеченных ключевым словом case с последующим константным-выражением. Использование целого константного выражения является существенным недостатком, присущим рассмотренному оператору. Так как константное выражение вычисляется во время трансляции, оно не может содержать переменные или вызовы функций. Обычно в качестве константного выражения используются целые или символьные константы. Все константные выражения в операторе switch должны быть уникальны. Кроме операторов, помеченных ключевым словом case, может быть, но обязательно один, фрагмент помеченный ключевым словом default. Список операторов может быть пустым, либо содержать один или более операторов. В операторе switch не требуется заключать в фигурные скобки последовательность операторов. В операторе switch можно использовать свои локальные переменные, объявления которых находятся перед первым ключевым словом case, однако в объявлениях не должна использоваться инициализация. Работает оператор switch следующим образом: 1. вычисляется выражение в круглых скобках; 2. вычисленные значения последовательно сравниваются с константными выражениями, следующими за ключевыми словами case; 3. если одно из константных выражений совпадает со значением выражения, то управление передается на оператор, помеченный соответствующим ключевым словом case; 4. если ни одно из константных выражений не равно выражению, то управление передается на оператор, помеченный ключевым словом default, а в случае его отсутствия управление передается на следующий после switch оператор. Особенность использования оператора switch: конструкция со словом default может быть не последней в теле оператора switch. Ключевые слова case и default в теле оператора switch существенны только при начальной проверке, когда определяется начальная точка выполнения тела оператора switch. Все операторы, между начальным оператором и концом тела, выполняются вне зависимости от ключевых слов, если только какой-то из операторов не передаст управления из тела оператора switch. Таким образом, программист должен сам позаботится о выходе из case, если это необходимо. Чаще всего для этого используется оператор break. Для того, чтобы выполнить одни и те же действия для различных значений выражения, можно пометить один и тот же оператор несколькими ключевыми словами case. Пример: int i=2; switch (i) { case 1: i += 2; case 2: i *= 3; case 0: i /= 2; case 4: i -= 5; default:; } Выполнение оператора switch начинается с оператора, помеченного case 2. Таким образом, переменная i получает значение, равное 6, далее выполняется оператор, помеченный ключевым словом case 0, а затем case 4, переменная i примет значение 3, а затем значение -2. Оператор, помеченный ключевым словом default, не изменяет значения переменной. Рассмотрим ранее приведенный пример, в котором иллюстрировалось использование вложенных операторов if, с использованием оператора switch. char ZNAC; int x,y,z; switch (ZNAC) { case '+': x = y + z; break; case '-': x = y - z; break; case '*': x = y * z; break; case '/': x = u / z; break; default : ; } Использование оператора break позволяет в необходимый момент прервать последовательность выполняемых операторов в теле оператора switch, путем передачи управления оператору, следующему за switch. В теле оператора switch можно использовать вложенные операторы switch, при этом в ключевых словах case можно использовать одинаковые константные выражения. Пример: : switch (a) { case 1: b=c; break; case 2: switch (d) { case 0: f=s; break; case 1: f=9; break; case 2: f-=9; break; } case 3: b-=c; break; : } Оператор case — это метка, однако он не может быть использован сам по себе, вне оператора switch. Оператор break — это один из операторов безусловного перехода. Он может применяться не только в операторе switch, но и в циклах. Когда в теле оператора switch встречается оператор break, программа выходит из оператора switch и выполняет оператор, следующий за фигурной скобкой } оператора switch. Особенности оператора switch: 1. оператор switch отличается от if тем, что в нем управляющее выражение проверяется только на равенство с постоянными, в то время как в if проверяется любой вид отношения или логического выражения; 2. в одном и том же операторе switch никакие два оператора case не могут иметь равных постоянных. Конечно, если один switch вложен в другой, в их операторах case могут быть совпадающие постоянные; 3. если в управляющем выражении оператора switch встречаются символьные константы, они автоматически преобразуются к целому типу по принятым в языке С правилам приведения типов. Оператор switch часто используется для обработки команд с клавиатуры, например, при выборе пунктов меню. В следующем примере программа выводит на экран меню проверки правописания и вызывает соответствующую процедуру: void menu(void) { char ch; printf("1. Проверка правописания\n"); printf("2. Коррекция ошибок\n"); printf("3. Вывод ошибок\n"); printf("Для пропуска нажмите любую клавишу\n"); printf(" Введите Ваш выбор: "); ch = getchar(); /* чтение клавиш */ switch(ch) { case '1': check_spelling(); break; case '2': correct_errors(); break; case '3': display_errors(); break; default : printf("Ни выбрана ниодна опция"); } } С точки зрения синтаксиса, присутствие операторов break внутри switch не обязательно. Они прерывают выполнение последовательности операторов, ассоциированных с данной константой. Если оператор break отсутствует, то выполняется следующий оператор case, пока не встретится очередной break, или не будет достигнут конец тела оператора switch. Например, в функции inp_handler() (обработчик ввода драйвера) для упрощения программы несколько операторов break опущено, поэтому выполняются сразу несколько операторов case: /* Обработка значения i */ void inp_handler(int i) { int flag; flag = -1; switch(i) { case 1: /* Эти case эти общую */ case 2: /* последовательность операторов. */ case 3: flag = 0; break; case 4: flag = 1; case 5: error(flag); break; default: process(i); } } Приведенный пример иллюстрирует следующие две особенности оператора switch(). Во-первых, оператор case может не иметь ассоциированной с ним последовательности операторов. Тогда управление переходит к следующему case. В этом примере три первых case вызывают выполнение одной и той же последовательности операторов, а именно: flag = 0; break; Во-вторых, если оператор break отсутствует, то выполняется последовательность операторов следующего case. Если i равно 4, то переменной flag присваивается значение 1 и, поскольку break отсутствует, выполнение продолжается и вызывается error(flag). Если i равно 5, то error() будет вызвана со значением переменной flag, равным —1, а не 1. То, что при отсутствии break операторы case выполняются вместе, позволяет избежать ненужного дублирования операторов. Вложенные операторы switch Оператор switch может находиться в теле внешнего по отношению к нему оператора switch. Операторы case внутреннего и внешнего switch могут иметь одинаковые константы, в этом случае они не конфликтуют между собой. Например, следующий фрагмент программы вполне работоспособен: switch(x) { case 1: switch(y) { case 0: printf("Деление на нуль.\n"); break; case 1: process(x,y); break; } break; case 2:… Согласно Стандарту С89, оператор switch может иметь как минимум 257 операторов case. Стандарт С99 требует поддержки как минимум 1023 операторов case. Тестирование разветвленных алгоритмов Тестирование сколь угодно сложных конструкций сводится к тестированию базовых конструкций. Уточним требования к соответствующим тестам. Существуют различные критерии качества структурного тестирования. Мы будем использовать критерий комбинаторного покрытия условий: каждое простое условие (логическая переменная или отношение) должно выполниться по крайней мере один раз. Развилка. Тест должен обеспечить выполнение каждого простого условия. Если условие содержит k простых подусловий, то общее число возможных комбинаций значений подусловий 2 в степени K. Реально из этих комбинаций выбрасываются невыполнимые или неинформативные. Например, в конструкции если ((a>2) & (a<10)) то p:=1 иначе p:=0 если; для прохождения ветви «то» может быть построен один тест, где оба подусловия истинны. Условие прохождения ветви «иначе» - отрицание условия (a>2) & (a<10), т.е. условие (a<=2) Ú (a>=10). Таким образом, для тестирования ветви «иначе» нужны два теста, в одном из которых a<=2, а в другом - a>=10. Оба эти подусловия в данном случае одновременно истинными быть не могут. 12 Основные управляющие алгоритмические структуры Цикл: цикл с параметром (ДЛЯ); цикл с предусловием (ПОКА); цикл с постусловием (ПОВТОРЯТЬ-ДО). Правила организации циклических алгоритмов. Проблемы и методика тестирования циклических алгоритмов В языке С, как и в других языках программирования, операторы цикла служат для многократного выполнения последовательности операторов до тех пор, пока выполняется некоторое условие. Условие может быть установленным заранее (как в операторе for) или меняться при выполнении тела цикла (как в while или do-while). К основным группам задач для использования циклических алгоритмов относятся: • организация контроля за вводом; • многоразовое выполнение программы; • вычисление функций от одного или более аргументов y=F(x) с подзадачей вывода результата на экран: в форме таблицы (табулирование) или в виде графиков; • итерационные алгоритмы – вычисление значений с заданной точностью (нахождение корней уравнений, значений функций, многочленов); • обработка табличных данных в виде массивов данных. Цикл for Во всех процедурных языках программирования циклы for очень похожи. Однако в С этот цикл особенно гибкий и мощный. Общая форма оператора for следующая: for (инициализация; условие; приращение) оператор; Существенно то, что проверка условия всегда выполняется в начале цикла. Это значит, что тело цикла может ни разу не выполниться, если условие выполнения сразу будет ложным. Цикл for может иметь большое количество вариаций. В наиболее общем виде принцип его работы следующий. Инициализация — это присваивание начального значения переменной, которая называется параметром цикла. Условие представляет собой условное выражение, определяющее, следует ли выполнять оператор цикла (часто его называют телом цикла) в очередной раз. Оператор приращение осуществляет изменение параметра цикла при каждой итерации. Эти три оператора (они называются также секциями оператора for) обязательно разделяются точкой с запятой. Цикл for выполняется, если выражение условие принимает значение ИСТИНА. Если оно хотя бы один раз примет значение ЛОЖЬ, то программа выходит из цикла и выполняется оператор, следующий за телом цикла for. В следующем примере в цикле for выводятся на экран числа от 1 до 100: #include int main(void) { int x; for(x=1; x <= 100; x++) printf("%d ", x); return 0; } В этом примере параметр цикла х инициализирован числом 1, а затем при каждой итерации сравнивается с числом 100. Пока переменная х меньше 100, вызывается функция printf() и цикл повторяется. При этом х увеличивается на 1 и опять проверяется условие цикла х <= 100. Процесс повторяется, пока переменная х не станет больше 100. После этого процесс выходит из цикла, а управление передается оператору, следующему за ним. В этом примере параметром цикла является переменная х, при каждой итерации она изменяется и проверяется в секции условия цикла. В следующем примере в цикле for выполняется блок операторов: for(x=100; x != 65; x -= 5) { z = x*x; printf("Квадрат %d равен %d", x, z); } Операции возведения переменной х в квадрат и вызова функции printf() повторяются, пока х не примет значение 65. Здесь параметр цикла уменьшается, он инициализирован числом 100 и уменьшается на 5 при каждой итерации. В операторе for условие цикла всегда проверяется перед началом итерации. Это значит, что операторы цикла могут не выполняться ни разу, если перед первой итерацией условие примет значение ЛОЖЬ. Например, в следующем фрагменте программы x = 10; for(y=10; y!=x; ++y) printf("%d", y); printf("%d", y); /* Это единственный printf() который будет выполнен */ цикл не выполнится ни разу, потому что при входе в цикл значения переменных х и у равны. Поэтому условие цикла принимает значение ЛОЖЬ, а тело цикла и оператор приращение не выполняются. Переменная у остается равной 10, единственный результат работы этой программы — вывод на экран числа 10 в результате вызова функции printf(), расположенной вне цикла. Варианты цикла for В языке С допускаются некоторые его варианты, позволяющие во многих случаях увеличить мощность и гибкость программы. Применение операции последовательного вычисления Один из распространенных способов усиления мощности цикла for — применение операции последовательного вычисления для создания двух параметров цикла. Данная операция связывает несколько выражений, заставляя их выполняться вместе. В следующем примере обе переменные (х и у) являются параметрами цикла for и обе инициализируются в этом цикле: for(x=0, y=0; x+y<10; ++x) { y = getchar(); y = y - '0'; /* Вычитание из y ASCII-кода нуля */ } Здесь запятая разделяет два оператора инициализации. При каждой итерации значение переменной х увеличивается, а значение у вводится с клавиатуры. Для выполнения итерации как х, так и у должны иметь определенное значение. Несмотря на то, что значение у вводится с клавиатуры, оно должно быть инициализировано таким образом, чтобы выполнилось условие цикла при первой итерации. Если у не инициализировать, то оно может случайно оказаться таким, что условие цикла примет значение ЛОЖЬ, тело цикла не будет выполнено ни разу. Следующий пример демонстрирует использование двух параметров цикла. Функция converge() копирует содержимое одной строки в другую, начиная с обоих концов строки и кончая в ее середине. /* Демонстрация использования 2-х параметров цикла. */ #include #include void converge(char *targ, char *src); int main(void) { char target[80] = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; converge(target, "Это проверка функции converge()."); printf("Строка-результат: %s\n", target); return 0; } /* Эта функция копирует содержимое одной строки в другую, начиная с обоих концов и сходясь посередине. */ void converge(char *targ, char *src) { int i, j; printf("%s\n", targ); for(i=0, j=strlen(src); i<=j; i++, j--) { targ[i] = src[i]; targ[j] = src[j]; printf("%s\n", targ); } } Программа выводит на экран следующее: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ЭXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ЭтХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ. ЭтоХХХХХХХХХХХХХХХХХХХХХХХХХХХ). Это ХХХХХХХХХХХХХХХХХХХХХХХХХ(). Это пХХХХХХХХХХХХХХХХХХХХХХХe(). Это прХХХХХХХХХХХХХХХХХХХХХge(). Это проХХХХХХХХХХХХХХХХХХХrge(). Это провХХХХХХХХХХХХХХХХХerge(). Это провeXXXXXXXXXXXXXXXverge(). Это провepXXXXXXXXXXXXXnverge(). Это провepKXXXXXXXXXXXonverge(). Это провepкaXXXXXXXXXconverge(). Это проверка ХХХХХХХ converge(). Это проверка фХХХХХи converge(). Это проверка фуХХХии converge(). Это проверка фунХции converge(). Это проверка функции converge(). Строка-результат: Это проверка функции converge(). В функции convergence() цикл for использует два параметра цикла (i и j) для индексации строки с противоположных концов. Параметр i в цикле увеличивается, а j — уменьшается. Итерации прекращаются, когда i становится больше j. Это обеспечивает копирование всех символов. Проверка параметра цикла на соответствие некоторому условию не обязательна. Условие может быть любым логическим оператором или оператором отношения. Это значит, что условие выполнения цикла может состоять из нескольких условий, или операторов отношения. Следующий пример демонстрирует применение составного условия цикла для проверки пароля, вводимого пользователем. Пользователю предоставляются три попытки ввода пароля. Программа выходит из цикла, когда использованы все три попытки или когда введен верный пароль. void sign_on(void) { char str[20]; int x; for(x=0; x<3 && strcmp(str, "password"); ++x) { printf("Пожалуйста, введите пароль:"); gets(str); } if(x==3) return; /* Иначе пользователь допускается */ } Функция sign_on() использует стандартную библиотечную функцию strcmp(), которая сравнивает две строки и возвращает 0, если они совпадают. Каждая из трех секций оператора for может быть любым синтаксически правильным выражением. Эти выражения не всегда каким-либо образом отображают назначение секции. Рассмотрим следующий пример: #include int sqrnum(int num); int readnum(void); int prompt(void); int main(void) { int t; for(prompt(); t=readnum(); prompt()) sqrnum(t); return 0; } int prompt(void) { printf("Введите число: "); return 0; } int readnum(void) { int t; scanf("%d", &t); return t; } int sqrnum(int num) { printf("%d\n", num*num); return num*num; } Здесь в main() каждая секция цикла for состоит из вызовов функций, которые предлагают пользователю ввести число и считывают его. Если пользователь ввел 0, то цикл прекращается, потому что тогда условие цикла принимает значение ЛОЖЬ. В противном случае число возводится в квадрат. Таким образом, в этом примере цикла for секции инициализации и приращения используются весьма необычно, но совершенно правильно. Другая интересная особенность цикла for состоит в том, что его секции могут быть вообще пустыми, присутствие в них какого-либо выражения не обязательно. В следующем примере цикл выполняется, пока пользователь не введет число 123: for(x=0; x!=123; ) scanf("%d", &x); Секция приращения оператора for здесь оставлена пустой. Это значит, что перед каждой итерацией значение переменной х проверяется на неравенство числу 123, а приращения не происходит, оно здесь ненужно. Если с клавиатуры ввести число 123, то условие принимает значение ЛОЖЬ и программа выходит из цикла. Инициализацию параметра цикла for можно сделать за пределами этого цикла, но, конечно, до него. Это особенно уместно, если начальное значение параметра цикла вычисляется достаточно сложно, например: gets(s); /* читает строку в s */ if(*s) x = strlen(s); /* вычисление длины строки */ else x = 10; for( ; x<10; ) { printf("%d", x); ++x; } В этом примере секция инициализации оставлена пустой, а переменная х инициализируется до входа в цикл. Бесконечный цикл Другим вариантом использования оператора for является бесконечный цикл. Для организации такого цикла можно использовать пустое условное выражение, а для выхода из цикла обычно используют дополнительное условие и оператор break. Пример: for (;;) { ... ... break; ... } Так как согласно синтаксису языка Си оператор может быть пустым, тело оператора for также может быть пустым. Так как в операторе for может отсутствовать любая секция, бесконечный цикл проще всего сделать, оставив пустыми все секции. Это хорошо показано в следующем примере: for( ; ; ) printf("Этот цикл является бесконечным.\n"); Если условие цикла for отсутствует, то предполагается, что его значение — ИСТИНА. В оператор for можно добавить выражения инициализации и приращения, хотя обычно для создания бесконечного цикла используют конструкцию for( ; ; ). Фактически конструкция for( ; ; ) не гарантирует бесконечность итераций, потому что в нем может встретиться оператор break, вызывающий немедленный выход из цикла. В этом случае выполнение программы продолжается с оператора, следующего за закрывающейся фигурной скобкой цикла for: ch = '\0'; for( ; ; ) { ch = getchar(); /* считывание символа */ if(ch=='A') break; /* выход из цикла */ } printf("Вы напечатали 'A'"); В данном примере цикл выполняется до тех пор, пока пользователь не введет с клавиатуры символ А. Цикл for без тела цикла Следует учесть, что оператор может быть пустым. Это значит, что тело цикла for (или любого другого цикла) также может быть пустым. Такую особенность цикла for можно использовать для упрощения некоторых программ, а также в циклах, предназначенных для того, чтобы отложить выполнение последующей части программы на некоторое время. Программисту иногда приходится решать задачу удаления пробелов из входного потока. Допустим, программа, работающая с базой данных, обрабатывает запрос "показать все балансы меньше 400". База данных требует представления каждого слова отдельно, без пробелов, т.е. обработчик распознает слово "показать", но не " показать". В следующем примере цикл for удаляет начальные пробелы в строке str: for( ; *str == ' '; str++) ; В этом примере указатель str переставляется на первый символ, не являющийся пробелом. Цикл не имеет тела, так как в нем нет необходимости. Иногда возникает необходимость отложить выполнение последующей части программы на определенное время. Это можно сделать с помощью цикла for следующим образом: for(t=0; t #include void pad(char *s, int length); int main(void) { char str[80]; strcpy(str, "это проверка"); pad(str, 40); printf("%d", strlen(str)); return 0; } /* Добавление пробелов в конец строки. */ void pad(char *s, int length) { int l; l = strlen(s); /* опредление длины строки */ while(l 100); Цикл do-while часто используется в функциях выбора пунктов меню. Если пользователь вводит допустимое значение, оно возвращается в качестве значения функции. В противном случае цикл требует повторить ввод. Следующий пример демонстрирует усовершенствованную версию программы для выбора пункта меню проверки грамматики: void menu(void) { char ch; printf("1. Проверка правописания\n"); printf("2. Коррекция ошибок\n"); printf("3. Вывод ошибок\n"); printf(" Введите Ваш выбор: "); do { ch = getchar(); /* чтение выбора с клавиатуры */ switch(ch) { case '1': check_spelling(); break; case '2': correct_errors(); break; case '3': display_errors(); break; } } while(ch!='1' && ch!='2' && ch!='3'); } В этом примере применение цикла do-while весьма уместно, потому что итерация всегда должна выполниться как минимум один раз. Цикл повторяется, пока его условие не станет ложным, т.е. пока пользователь не введет один из допустимых ответов. Пример: int i,j,k; ... i=0; j=0; k=0; do { i++; j--; while (a[k] < i) k++; } while (i<30 && j<-30); Вложенность операторов цикла Существует возможность организовать цикл внутри тела другого цикла. Такой цикл будет называться вложенным циклом. Вложенный цикл по отношению к циклу, в тело которого он вложен, будет именоваться внутренним циклом, и наоборот цикл в теле которого существует вложенный цикл будет именоваться внешним по отношению к вложенному. Внутри вложенного цикла в свою очередь может быть вложен еще один цикл, образуя следующий уровень вложенности и так далее. Количество уровней вложенности, как правило, не ограничивается. Операторы continue и break в операторах цикла В теле цикла могут использоваться операторы break и continue. Оператор break обеспечивает немедленный выход из цикла, оператор continue вызывает прекращение очередной и начало следующей итерации, но по правилам структурного программирования они запрещены!!!. С точки зрения структурного программирования команды досрочного выхода из цикла и продолжения итерации являются избыточными, поскольку их действие может быть легко смоделировано чисто структурными средствами. Более того, по мнению ряда теоретиков программирования (в частности, Эдсгера Дейкстры), сам факт использования в программе неструктурных средств, будь то классический безусловный переход или любая из его специализированных форм, таких как break или continue, является свидетельством недостаточно проработанного алгоритма решения задачи. Однако на практике код программы часто является записью уже имеющегося, ранее сформулированного алгоритма, перерабатывать который нецелесообразно по чисто техническим причинам. Попытка заменить в таком коде команду досрочного выхода на структурные конструкции часто оказывается неэффективной или громоздкой. Например, вышеприведённый фрагмент кода с командой break может быть записан так: // Досрочный выход из цикла без break bool flag = false; // флаг досрочного завершения while(<условие> && !flag) { ... операторы if (<ошибка>) { flag = true; } else { ... операторы } } ... продолжение программы Легко убедиться, что фрагмент будет работать аналогично предшествующим, разница лишь в том, что в месте проверки на ошибку вместо непосредственного выхода из цикла устанавливается флаг досрочного выхода, который проверяется позже в штатном условии продолжения цикла. Однако для отказа от команды досрочного выхода пришлось добавить в программу описание флага и вторую ветвь условного оператора, к тому же произошло «размытие» логики программы (решение о досрочном выходе принимается в одном месте, а выполняется в другом). В результате программа не стала ни проще, ни короче, ни понятнее. Несколько иначе обстоит дело с командой пропуска итерации. Она, как правило, очень легко и естественно заменяется на условный оператор. Например, приведённый выше фрагмент суммирования массива можно записать так: int arr[ARRSIZE]; ... // Суммирование отдельно всех и только положительных // элементов массива arr с заменой continue int sum_all = 0; int sum_pos = 0; for (int i = 0 ; i < ARRSIZE; ++i) { sum_all += arr[i]; if (arr[i] > 0) // Условие заменено на противоположное! { sum_pos += arr[i]; } } Как видим, достаточно было заменить проверяемое условие на противоположное и поместить заключительную часть тела цикла в условный оператор. Можно заметить, что программа стала короче (за счёт удаления команды пропуска итерации) и одновременно логичнее (из кода непосредственно видно, что суммируются положительные элементы). Несмотря на свою ограниченную полезность и возможность замены на другие языковые конструкции, команды пропуска итерации и, особенно, досрочного выхода из цикла в отдельных случаях оказываются крайне полезны, именно поэтому они сохраняются в современных языках программирования. 13 Разработка циклических алгоритмов при работе с простыми данными: контролируемый ввод; итерации и рекурсия; создание диалоговых программ Правила организации циклических алгоритмов Общие правила использования операторов цикла: • если количество повторов известно заранее, используется оператор Для (for), • если количество повторов неизвестно, применяются операторы Повторять_До (do while) или Пока(while do).Основным отличием между циклами while и do - while является то, что тело в цикле do - while выполняется по крайней мере один раз. При использовании операторов цикла существует проблема зацикливания, то есть может сложиться ситуация, когда из цикла нет выхода. При организации циклических алгоритмов необходимо учитывать следующее: • все переменные, входящие в логическое выражение, должны быть инициализированы перед входом в оператор цикла; • в теле цикла должна выполняться хотя бы одна операция (оператор), при помощи которого логическое выражение меняет свое значение на противоположное. Причем, за конечное число шагов; • необходимо следить за тем, чтобы при выполнении операций, входящих в тело цикла, не происходило переполнение разрядной сетки или потери значимости; Лекция 13. Разработка циклических алгоритмов при работе с простыми данными: контролируемый ввод; итерации и рекурсия; создание диалоговых программ 2 Контролируемый ввод Идея алгоритма заключается в следующем. Считывание данных, введенных пользователем, осуществляем в строку. Пытаемся преобразовать строку в число. Если строка является неправильной формой записи числа, то выведем сообщении об ошибке и повторим ввод данных, иначе –выходим из цикла. Кроме проверки цифрового формата введенных данных можно осуществить проверку числового значения на принадлежность диапазону допустимых значений. Алгоритм InpFloat; Вход: min,max:float; /* границы диапазона*/ Выход: xx:float Локальные переменные: st:char[]; code:int;/* код ошибки преобразования. В некоторых функциях отсутствие ошибки – Null*/ Начало Повторять Нц Ввод строки (st) ; Преобразуем st в хх с кодом ошибки code; Если (сode=0) или (ххmax) То начало Вывод(‘Ошибка ввода. ENTER - далее’); Повторять Нц Считать код клавиши в ch; Кц До (ch=13); Все {если} Кц До (code=0) и (xx>=min) и (xx<=max); Конец Создание диалоговых программ Пример создания цифрового меню Организация работы цифрового меню довольно проста. Формируется интерфейс меню. В цикле считывается код нажатой клавиши, соответственно выполняется оператор вызова той или иной функции, в случае выбора пункта меню «Выход» происходит выход из цикла, работа программы завершается. Рис. Интерфейс меню Повторять Нц Очистить экран; Сформировать интерфейс меню; Прочитать код нажатой клавиши в CH; Выбор ch из 1: начало Вызов функции ввода данных; конец; 2: начало Вызов функции вывода результата; конец; все{выбор}; кц До ch>=3; Пример создания циклического меню Работа данного вида меню основывается на обработке расширенного кода нажатой клавиши. Дано: N пунктов меню, массив [1..N] наименований пунктов меню, переменная punct – номер активного (выделенного другим цветом) пункта меню. Работа меню: Формируется интерфейс меню, по умолчанию активным является первый пункт меню. Ожидается нажатие клавиши пользователем. При нажатии клавиши «стрелка вниз»: экран очищается, формируется интерфейс меню, делается активным следующий пункт меню. Цикличность меню заключается в том, что если активным был последний пункт меню и при этом нажата клавиша «стрелка вниз», активным должен стать пункт меню №1 При нажатии клавиши «стрелка вверх»: экран очищается, формируется интерфейс меню, делается активным предыдущий пункт меню. Причем, если активным был первый пункт меню и была нажата клавиша «стрелка вверх», активным должен стать пункт меню № N. При нажатии Enter выполняется действие, соответствующее выбранному пункту меню. В программе используются пользовательские типы str=string[12] –наименование пункта меню - строка; arr=array[1..n] of str – массив наименований пунктов меню; Глобальные переменные menu:arr Назначение переменной Имя в программе Тип Диапазон Простая/ структура Вход / выход /константа цвет невыделенного пункта Normal; blue константа цвет выделенного пункта Select lightblue константа Количество пунктов меню N 3 константа координаты первой строки меню Х, У 5 константа названия пунктов меню menu arr структура Декомпозиция Что выполняет функцияа Имя в программе Вход : тип Выход : тип Примечание Ввод данных СallProc1 X, Y:float /возвращаемые функцией данные*/ Вывод данных СallProc2 X,Y:float Result:float Интерфейс меню с активным пунктом punct MenuToScr Punct:char Организация работы меню СyclMenu Х,у, rez:float; Алгоритм функции MenuToScr;/* вывод меню на экран*/ Вход: int punkt; Выход; Локальные переменные int i; Начало Очистить экран; Вывод(“ Действие 1 “); Вывод(“Действие 2 “); Вывод(“Выход “); изменить цвет текста (выделить); Выбор punkt из 1:начало Вывод(“ Действие 1 “); конец 2:начало Вывод(“Действие 2 “); конец 3:начало Вывод(“Выход “); конец все {выбор) восстановить основной цвет текста Конец Алгоритм функции CyclMenu Вход Выход x,y,rez:float; Локальные переменные punkt:int; /* номер выделенного пункта */ ch:char; /* введенный символ */ Начало punkt:=1; Повторять нц MenuToScr(punkt); Прочитать код нажатой клавиши в ch; Выбор ch из начало 80:/* стрелка вниз */ Если punkt1 то начало punkt:=punkt-1; конец иначе начало punkt:=n; конец; все (если); иначе /*обработка простого кода нажатой клавиши*/ Если ch= 13 то начало /* нажата клавиша */ case punkt of 1:CallProc1(x,y); 2:CallProc2(x,y,rez); 3:ch:=(27);/* выход */ конец; Все (если); все (выбор) кц до (ch=27);/* 27 - код */ конец; начало CyclMenu(x,y,rez); конец Циклические алгоритмы Алгоритм циклической структуры предусматривает многократное повторение действий в одной и той же последовательности по одним и тем же математическим зависимостям, но при разных значениях некоторой специально изменяемой величины. Циклические алгоритмы позволяют существенно сократить объем программы за счет многократного выполнения группы повторяющихся вычислений, так называемого тела цикла. Специально изменяемый по заданному закону параметр, входящий в тело цикла, называется переменной цикла. Переменная цикла используется для подготовки очередного повторения цикла и отслеживания условий его окончания. В качестве переменной цикла используют любые переменные, индексы массивов, аргументы вычисляемых функций и тому подобные величины. Во время выполнения тела цикла параметры переменной цикла изменяются в интервале от начального до конечного значения с заданным шагом. Следовательно, при организации циклических вычислений необходимо предусмотреть задание начального значения переменной цикла, закон ее изменения перед каждым новым повторением и ее конечное значение, при достижении которого произойдет завершение цикла. Циклы, в теле которых нет разветвлений и других встроенных в них циклов, называют простыми. В противном случае их относят к сложным. Циклические алгоритмы разделяют на детерминированные и итерационные. Циклы, в которых число повторений заранее известно из исходных данных или определено в ходе решения задачи, называют детерминированными. Для организации детерминированных циклов наиболее целесообразно использовать блок модификации, внутри которого указывается переменная цикла, ее начальное и конечное значения, а также шаг ее изменения (если шаг изменения равен 1, то его допускается не указывать). Организовать подобный цикл возможно и при использовании блока проверки условия вместо блока модификации, однако при этом несколько усложняется алгоритм и теряется его рациональность. Пример Дано натуральное число N. Найти сумму первых N членов натурального ряда. Варианты схемы алгоритма циклической структуры решения поставленной задачи приведены на рис. При этом в схеме : а - цикл организован с использованием блока модификации, б - блока проверки условия. В обоих алгоритмах операция нахождения суммы, при предварительном обнулении значения переменной S (блок 3), повторяется N раз. Рис. Схема алгоритма циклической структуры для нахождения суммы N первых членов натурального ряда: а) с использованием блока модификации; б) с использованием блока проверки условия В теле цикла использована операция присваивания S = S + I, по которой и осуществляется вычисление суммы путем прибавления к предыдущему значению переменной S всё новых значений переменной I. Цикл_является детерминированным и количество его повторений заранее определено (N раз). В качестве переменной цикла I принято текущее значение членов натурального ряда. Использование алгоритма с блоком модификации предпочтительнее, так как он обладает лучшей наглядностью. Циклы, в которых число повторений неизвестно из исходных данных и не определено по ходу решения задачи, называют итерационными. В итерационных циклах для организации выхода из тела цикла предусматривается проверка некоторого заранее заданного условия, для чего используют блок проверки условия. В итерационных циклах невозможно использовать блоки модификации, так как при организации таких циклов заранее неизвестно количество изменений переменной цикла и ее конечное значение. В зависимости от местонахождения блока проверки условия итерационные циклы могут быть организованы как циклы с предусловием (блок проверки условия размещен перед телом цикла) или с постусловием (блок проверки условия размещен после тела цикла). Если в цикле с предусловием входящие в тело цикла команды могут не выполняться ни разу (если начальное значение параметра цикла удовлетворяет условию выхода из цикла), то в цикле с постусловием - выполняются как минимум один раз (даже если начальное значение параметра цикла удовлетворяет условию выхода из него). Пример Дан ряд натуральных чисел 1,2,3,..., ∞ и число N. Требуется найти сумму первых членов ряда. Вычисление суммы прекратить, как только ее значение будет равно или превысит заданное N. Вариант схемы алгоритма решения задачи, включающей итерационный цикл с постусловием, приведен на рис. Цикл организован в виде итерационного потому, что число его повторений заранее неизвестно. В алгоритме выход из цикла или его продолжение определяется выполнением (или невыполнением) условия S>=N в блоке 6. Если условие не выполняется, то вычисление суммы продолжается путем прибавления к предыдущему значению суммы (переменной S) значения очередного члена ряда, отслеживаемого переменной цикла I. Однако приведенный алгоритм дает неверное решение при N=0, так как в этом случае не будет выполнено первое по порядку сравнение S=N=0. Правильный алгоритм решения этой задачи с использованием итерационного цикла с предусловием приведен в части 6 рисунка. В этом алгоритме тело цикла расположено после проверки условия выхода из него. В нем в начале цикла осуществляется проверка S>=N (блок 5) и если N задано равным нулю, то тело цикла не будет выполняться ни разу. Вид итерационного цикла (с пост- или предусловием) определяется условием задачи и допустимыми или возможными значениями исходных данных. Например, при решении задачи возможно использование итерационного цикла только с нижним окончанием, так как, для того чтобы выполнить проверку на окончание счета, необходимо сравнить значения двух членов ряда. Рис. Схема итерационного алгоритма нахождения суммы первых членов натурального ряда: а) – с постусловием; б) - с предусловием Пример Дано натуральное N и первый член бесконечного ряда: Y1=1. Вычислить сумму членов бесконечного ряда, образованного по следующему рекуррентному соотношению: Y1 =2 • YI-1, (то есть S = 1+2+4+8+16+...). Вычисление суммы продолжать до тех пор, пока соблюдается условие | Y1 - YI-1| int main(void) { int t; for(t=0; t<100; t++) { printf("%d ", t); if(t==10) break; } return 0; } выводит на экран числа от 0 до 10. После этого выполнение цикла прекращается оператором break, условие t < 100 при этом игнорируется. Оператор break часто используется в циклах, в которых некоторое событие должно вызвать немедленное прекращение выполнения цикла. В следующем примере нажатие клавиши прекращает выполнение функции look_up(): void look_up(char *name) { do { /* поиск имени 'name' */ if(kbhit()) break; } while(!found); /* process match */ } Библиотечная функция kbhit() возвращает 0, если клавиша не нажата (то есть, буфер клавиатуры пуст), в противном случае она возвращает ненулевое значение. В стандарте С функция kbhit() не определена, однако, практически, она поставляется почти с каждым компилятором (возможно, под несколько другим именем). Оператор break вызывает выход только из внутреннего цикла. Например, программа for(t=0; t<100; ++t) { count = 1; for(;;) { printf("%d ", count); count++; if(count==10) break; } } 100 раз выводит на экран числа от 1 до 9. Оператор break передает управление внешнему циклу for. Если оператор break присутствует внутри оператора switch, который вложен в какие-либо циклы, то break относится только к switch, выход из цикла не происходит. Функция exit() Функция exit() не является оператором языка, однако рассмотрим возможность ее применения. Аналогично прекращению выполнения цикла оператором break, можно прекратить работу программы и с помощью вызова стандартной библиотечной функции exit(). Эта функция вызывает немедленное прекращение работы всей программы и передает управление операционной системе. Общая форма функции exit() следующая: void exit (int код_возврата); Значение переменной код_возврата передается вызвавшему программу процессу, обычно в качестве этого процесса выступает операционная система. Нулевое значение кода возврата обычно используется для указания нормального завершения работы программы. Другие значения указывают на характер ошибки. В качестве кода возврата можно использовать макросы EXIT_SUCCESS и EXIT_FAILURE (выход успешный и выход с ошибкой). Функция exit() объявлена в заголовочном файле . Функция exit() часто используется, когда обязательное условие работы программы не выполняется. Рассмотрим, например, компьютерную игру в виртуальной реальности, использующую специальный графический адаптер. Главная функция main() этой игры выглядит так: #include int main(void) { if(!virtual_graphics()) exit(1); play(); /* ... */ } /* .... */ Здесь virtual_graphics() возвращает значение ИСТИНА, если присутствует нужный графический адаптер. Если требуемого адаптера нет, вызов функции exit(1) прекращает работу программы. В следующем примере в новой версии ранее рассмотренной функции menu() вызов exit() используется для выхода из программы и возврата в операционную систему: void menu(void) { char ch; printf("1. Проверка правописания\n"); printf("2. Коррекция ошибок\n"); printf("3. Вывод ошибок\n"); printf("4. Выход\n"); printf(" Введите Ваш выбор: "); do { ch = getchar(); /* чтение клавиши */ switch(ch) { case '1': check_spelling(); break; case '2': correct_errors(); break; case '3': display_errors(); break; case '4': exit(0); /* Возврат в ОС */ } } while(ch!='1' && ch!='2' && ch!='3'); } Оператор continue Можно сказать, что оператор continue похож на break. Оператор break вызывает прерывание цикла, a continue — прерывание текущей итерации цикла и осуществляет переход к следующей итерации. При этом все операторы до конца тела цикла пропускаются. В цикле for оператор continue вызывает выполнение операторов приращения и проверки условия цикла. В циклах while и do-while оператор continue передает управление операторам проверки условий цикла. В следующем примере программа подсчитывает количество пробелов в строке, введенной пользователем: /* Подсчет количества пробелов */ #include int main(void) { char s[80], *str; int space; printf("Введите строку: "); gets(s); str = s; for(space=0; *str; str++) { if(*str != ' ') continue; space++; } printf("%d пробелов\n", space); return 0; } Каждый символ строки сравнивается с пробелом. Если сравниваемый символ не является пробелом, оператор continue передает управление в конец цикла for и выполняется следующая итерация. Если символ является пробелом, значение переменной space увеличивается на 1. В следующем примере оператор continue применяется для выхода из цикла while путем передачи управления на условие цикла: void code(void) { char done, ch; done = 0; while(!done) { ch = getchar(); if(ch=='$') { done = 1; continue; } putchar(ch+1); /* печать следующего в алфавитном порядке символа */ } } Функция code предназначена для кодирования сообщения путем замены каждого символа символом, код которого на 1 больше кода исходного символа в коде ASCII. Например, символ А заменяется символом В (если это латинские символы.). Функция прекращает работу при вводе символа $. При этом переменной done присваивается значение 1 и оператор continue передает управление на условие цикла, что и прекращает выполнение цикла. Лекция 15. Функции: объявление и определение функции, класс памяти, тип возврата, глобальные переменные, формальные и фактические параметры, вызовы функций, вызовы с переменным числом аргументов, рекурсивные вызовы 4 Модуль 3. Модульное программирование. 15, 16 Функции В общем виде функция выглядит следующим образом: возвр-тип имя-функции(список параметров) { тело функции } возвр-тип определяет тип данного, возвращаемого функцией. Функция может возвращать любой тип данных, за исключением массивов список параметров — это список, элементы которого отделяются друг от друга запятыми. Каждый такой элемент состоит из имени переменной и ее типа данных. При вызове функции параметры принимают значения аргументов. Функция может быть и без параметров, тогда их список будет пустым. Такой пустой список можно указать в явном виде, поместив для этого внутри скобок ключевое слово void. В объявлениях (декларациях) переменных можно объявить (декларировать) несколько переменных одного и того же типа, используя для этого список одних только имен, элементы которого отделены друг от друга запятыми. А все параметры функций, наоборот, должны объявляться отдельно, причем для каждого из них надо указывать и тип, и имя. В общем виде список объявлений параметров должен выглядеть следующим образом: f(тип имя_переменной1, тип имя_переменной2,..., тип имя_переменнойN) Вот, например, два объявления параметров функций, первое из которых правильное, а второе — нет: f(int i, int k, int j) /* правильное */ f(int i, k, float j) /* неправильное, у переменной k должен быть собственный спецификатор типа */ Область действия функции В языке правила работы с областями действия — это правила, которые определяют, известен ли фрагменту кода другой фрагмент кода или данных, или имеет ли он доступ к этому другому фрагменту. Каждая функция представляет собой конечный блок кода. Таким образом, она определяет область действия этого блока. Это значит, что код функции является закрытым и недоступным ни для какого выражения из любой другой функции, если только не выполняется вызов содержащей его функции. (Например, нельзя перейти в середину другой функции с помощью goto.) Код, который составляет тело функции, скрыт от остальной части программы, и если он не использует глобальных переменных, то не может воздействовать на другие части программы или, наоборот, подвергаться воздействию с их стороны. Иначе говоря, код и данные, определенные внутри одной функции, без глобальных переменных не могут воздействовать на код и данные внутри другой функции, так как у любых двух разных функций разные области действия. Переменные, определенные внутри функции, являются локальными. Локальная переменная создается в начале выполнения функции, а при выходе из этой функции она уничтожается. Таким образом, локальная переменная не может сохранять свое значение в промежутках между вызовами функции. Единственное исключение из этого правила — переменные, объявленные со спецификатором класса памяти static. Таким переменным память выделяется так же, как и глобальным переменным, которые используются для хранения значений, но область действия таких переменных ограничена содержащими их функциями. Формальные параметры функции также находятся в ее области действия. Это значит, что параметр доступен внутри всей функции. Параметр создается в начале выполнения функции, и уничтожается при выходе из нее. Все функции имеют файл в качестве области действия (file scope). Таким образом, функцию нельзя определять внутри другой функции. Поэтому С практически не является языком с блочной структурой. Аргументы функции Если функция должна принимать аргументы, то в ее объявлении следует декларировать параметры, которые примут значения этих аргументов. Как видно из объявления следующей функции, объявления параметров стоят после имени функции. /* Возвращает 1, если символ c входит в строку s; и 0 в противном случае. */ int is_in(char *s, char c) { while(*s) if(*s==c) return 1; else s++; return 0; } Функция is_in() имеет два параметра: s и d. Если символ c входит в строку s, то эта функция возвращает 1, в противном случае она возвращает 0. Хотя параметры выполняют специальную задачу, — принимают значения аргументов, передаваемых функции, — они все равно ведут себя так, как и другие локальные переменные. Формальным параметрам функции, например, можно присваивать какие-либо значения или использовать эти параметры в каких-либо выражениях. Вызовы по значению и по ссылке В языках программирования имеется два способа передачи значений подпрограмме. Первый из них — вызов по значению. При его применении в формальный параметр подпрограммы копируется значение аргумента. В таком случае изменения параметра на аргумент не влияют. Вторым способом передачи аргументов подпрограмме является вызов по ссылке. При его применении в параметр копируется адрес аргумента. Это значит, что, в отличие от вызова по значению, изменения значения параметра приводят к точно таким же изменениям значения аргумента. За небольшим количеством исключений, в языке С для передачи аргументов используется вызов по значению. Обычно это означает, что код, находящийся внутри функции, не может изменять значений аргументов, которые использовались при вызове функции. #include int sqr(int x); int main(void) { int t=10; printf("%d %d", sqr(t), t); return 0; } int sqr(int x) { x = x*x; return(x); } В этом примере в параметр х копируется 10 — значение аргумента для sqr(). Когда выполняется присваивание х=х*х, модифицируется только локальная переменная х. А значение переменной t, использованной в качестве аргумента при вызове sqr(), по-прежнему остается равным 10. Поэтому выведено будет следующее: 100.10. Помните, что именно копия значения аргумента передается в функцию. А то, что происходит внутри функции, не влияет на значение переменной, которая была использована при вызове в качестве аргумента. Вызов по ссылке Хотя в С для передачи параметров применяется вызов по значению, можно создать вызов и по ссылке, передавая не сам аргумент, а указатель на него. Так как функции передается адрес аргумента, то ее внутренний код в состоянии изменить значение этого аргумента, находящегося, между прочим, за пределами самой функции. Указатель передается функции так, как и любой другой аргумент. Конечно, в таком случае параметр следует декларировать как один из типов указателей. Это можно увидеть на примере функции swap(), которая меняет местами значения двух целых переменных, на которые указывают аргументы этой функции: void swap(int *x, int *y) { int temp; temp = *x; /* сохранить значение по адресу x */ *x = *y; /* поместить y в x */ *y = temp; /* поместить x в y */ } Функция swap() может выполнять обмен значениями двух переменных, на которые указывают х и y, потому что передаются их адреса, а не значения. Внутри функции, используя стандартные операции с указателями, можно получить доступ к содержимому переменных и провести обмен их значений. Помните, что swap() (или любую другую функцию, в которой используются параметры в виде указателей) необходимо вызывать вместе с адресами аргументов. Следующая программа показывает, как надо правильно вызывать swap(): #include void swap(int *x, int *y); int main(void) { int i, j; i = 10; j = 20; printf("i и j перед обменом значениями: %d %d\n", i, j); swap(&i, &j); /* передать адреса переменных i и j */ printf("i и j после обмена значениями: %d %d\n", i, j); return 0; } void swap(int *x, int *y) { int temp; temp = *x; /* сохранить значение по адресу x */ *x = *y; /* поместить y в x */ *y = temp; /* поместить x в y */ } И вот что вывела эта программа: i и j перед обменом значениями: 10 20 i и j после обмена значениями: 20 10 В программе переменной i присваивается значение 10, а переменной j — значение 20. Затем вызывается функция swap() с адресами этих переменных. (Для получения адреса каждой из переменных используется унарный оператор &.) Поэтому в swap() передаются адреса переменных i и j, а не их значения. Язык C++ при помощи параметров-ссылок дает возможность полностью автоматизировать вызов по ссылке. А в языке С параметры-ссылки не поддерживается Вызов функций с помощью массивов Когда в качестве аргумента функции используется массив, то функции передается его адрес. В этом и состоит исключение по отношению к правилу, которое гласит, что при передаче параметров используется вызов по значению. В случае передачи массива функции ее внутренний код работает с реальным содержимым этого массива и вполне может изменить это содержимое. Проанализируйте, например, функцию print_upper(), которая печатает свой строковый аргумент на верхнем регистре: #include #include void print_upper(char *string); int main(void) { char s[80]; printf("Введите строку символов: "); gets(s); print_upper(s); printf("\ns теперь на верхнем регистре: %s", s); return 0; } /* Печатать строку на верхнем регистре. */ void print_upper(char *string) { register int t; for(t=0; string[t]; ++t) { string[t] = toupper(string[t]); putchar(string[t]); } } Вот что будет выведено в случае фразы "This is a test." (это тест): Введите строку символов: This is a test. THIS IS A TEST. s теперь в верхнем регистре: THIS IS A TEST. Правда, эта программа не работает с символами кириллицы. После вызова print_upper() содержимое массива s в main() переводится в символы верхнего регистра. Если вам это не нужно, программу можно написать следующим образом: #include #include void print_upper(char *string); int main(void) { char s[80]; printf("Введите строку символов: "); gets(s); print_upper(s); printf("\ns не изменялась: %s", s); return 0; } void print_upper(char *string) { register int t; for(t=0; string[t]; ++t) putchar(toupper(string[t])); } Вот какой на этот раз получится фраза "This is a test.": Введите строку символов: This is a test. THIS IS A TEST. s не изменилась: This is a test. На этот раз содержимое массива не изменилось, потому что внутри print_upper() не изменялись его значения. Классическим примером передачи массивов в функции является стандартная библиотечная функция gets(). Хотя gets(), которая находится в вашей стандартной библиотеке, и более сложная, чем предлагаемая вам версия xgets(), но с помощью функции xgets() вы сможете получить представление о том, как работает gets(). /* Упрощенная версия стандартной библиотечной функции gets(). */ char *xgets(char *s) { char ch, *p; int t; p = s; /* xgets() возвращает указатель s */ for(t=0; t<80; ++t){ ch = getchar(); switch(ch) { case '\n': s[t] = '\0'; /* завершает строку */ return p; case '\b': if(t>0) t--; break; default: s[t] = ch; } } s[79] = '\0'; return p; } Функцию xgets() следует вызывать с указателем char *. Им, конечно же, может быть имя символьного массива, которое по определению является указателем char *. В самом начале программы xgets() выполняется цикл for от 0 до 80. Это не даст вводить с клавиатуры строки, содержащие более 80 символов. При попытке ввода большего количества символов происходит возврат из функции. (В настоящей функции gets() такого ограничения нет.) Так как в языке С нет встроенной проверки границ, программист должен сам позаботиться, чтобы в любом массиве, используемом при вызове xgets(), помещалось не менее 80 символов. Когда символы вводятся с клавиатуры, они сразу записываются в строку. Если пользователь нажимает клавишу , то счетчик t уменьшается на 1, а из массива удаляется последний символ, введенный перед нажатием этой клавиши. Когда пользователь нажмет , в конец строки запишется нуль, т.е. признак конца строки. Так как массив, использованный для вызова xgets(), модифицируется, то при возврате из функции в нем будут находиться введенные пользователем символы. При передаче указателя будет применен вызов по значению, и сам указатель внутри функции вы изменить не сможете. Однако для того объекта, на который указывает этот указатель, все произойдет так, будто этот объект был передан по ссылке. В некоторых языках программирования (например, в Алголе-60) имелись специальные средства, позволяющие уточнить, как следует передавать аргументы: по ссылке или по значению. Благодаря наличию указателей в С механизм передачи параметров удалось унифицировать. Параметры, не являющиеся массивами, в С всегда вызываются только по значению, но все, что в других языках вы можете сделать с объектом, получив ссылку на него (т.е. его адрес), вы можете сделать, получив значение указателя на этот объект (т.е. опять же, его адрес). Так что в языке С благодаря свойственной ему унификации передачи параметров никаких проблем не возникает. А вот в других языках трудности, связанные с отсутствием эффективных средств работы с указателями, встречаются довольно часто. Задача, решаемая этой программой, кажется тривиальной. Ну разве представляет трудность написать на каком-либо процедурном языке, например, на Алголе-60, процедуру, которая обменивает значения своих параметров. Ведь так просто написать: procedure swap(x, y); integer х, y; начало integer t; t:= x; x:=y; y:=t end. Но эта процедура работает неправильно, хотя вызов значений здесь происходит по ссылке! Причем сразу найти тестовый пример, демонстрирующий ошибочность этой процедуры, удается далеко не всем. Ведь в случае вызова swap(i, j) все работает правильно! А что будет в случае вызова swap(i, a[i])? Да и можно ли на Алголе-60 вообще написать требуемую процедуру? Если вы склоняетесь к отрицательному ответу, то это показывает, насколько все-таки необходимы указатели в развитых языках программирования. Аргументы функции main(): argv и argc Иногда при запуске программы бывает полезно передать ей какую-либо информацию. Обычно такая информация передается функции main() с помощью аргументов командной строки. Аргумент командной строки — это информация, которая вводится в командной строке операционной системы вслед за именем программы. Например, чтобы запустить компиляцию программы, необходимо в командной строке после подсказки набрать примерно следующее: cc имя_программы имя_программы представляет собой аргумент командной строки, он указывает имя той программы, которую вы собираетесь компилировать. Чтобы принять аргументы командной строки, используются два специальных встроенных аргумента: argc и argv. Параметр argc содержит количество аргументов в командной строке и является целым числом, причем он всегда не меньше 1, потому что первым аргументом считается имя программы. А параметр argv является указателем на массив указателей на строки. В этом массиве каждый элемент указывает на какой-либо аргумент командной строки. Все аргументы командной строки являются строковыми, поэтому преобразование каких бы то ни было чисел в нужный двоичный формат должно быть предусмотрено в программе при ее разработке. Вот простой пример использования аргумента командной строки. На экран выводятся слово Привет и ваше имя, которое надо указать в виде аргумента командной строки. #include #include int main(int argc, char *argv[]) { if(argc!=2) { printf("Вы забыли ввести свое имя.\n"); exit(1); } printf("Привет %s", argv[1]); return 0; } Если вы назвали эту программу name (имя) и ваше имя Том, то для запуска программы следует в командную строку ввести name Том. В результате выполнения программы на экране появится сообщение Привет, Том. Во многих средах все аргументы командной строки необходимо отделять друг от друга пробелом или табуляцией. Запятые, точки с запятой и тому подобные символы разделителями не считаются. Например, run Spot, run состоит из трех символьных строк, в то время как Эрик, Рик, Фред представляет собой одну символьную строку — запятые, как правило, разделителями не считаются. Если в строке имеются пробелы, то, чтобы из нее не получилось несколько аргументов, в некоторых средах эту строку можно заключать в двойные кавычки. В результате вся строка будет считаться одним аргументом. Очень важно правильно объявлять argv. Вот как это делают чаще всего: char *argv[]; Пустые квадратные скобки указывают на то, что у массива неопределенная длина. Теперь получить доступ к отдельным аргументам можно с помощью индексации массива argv. Например, argv[0] указывает на первую символьную строку, которой всегда является имя программы; argv[1] указывает на первый аргумент и так далее. Другим небольшим примером использования аргументов командной строки является приведенная далее программа countdown (счет в обратном порядке). Эта программа считает в обратном порядке, начиная с какого-либо значения (указанного в командной строке), и подает звуковой сигнал, когда доходит до 0. Первый аргумент, содержащий начальное значение, преобразуется в целое значение с помощью стандартной функции atoi(). Если вторым аргументом командной строки (а если считать аргументом имя программы, то третьим) является строка "display" (вывод на экран), то результат отсчета (в обратном порядке) будет выводиться на экран. /* Программа счета в обратном порядке. */ #include #include #include #include int main(int argc, char *argv[]) { int disp, count; if(argc<2) { printf("В командной строке необходимо ввести число, с которого\n"); printf("начинается отсчет. Попробуйте снова.\n"); exit(1); } if(argc==3 && !strcmp(argv[2], "display")) disp = 1; else disp = 0; for(count=atoi(argv[1]); count; --count) if(disp) printf("%d\n", count); putchar('\a'); /* здесь подается звуковой сигнал */ printf("Счет закончен"); return 0; } Если аргументы командной строки не будут указаны, то будет выведено сообщение об ошибке. В программах с аргументами командной строки часто делается следующее: в случае, когда пользователь запускает эти программы без ввода нужной информации, выводятся инструкции о том, как правильно указывать аргументы. Чтобы получить доступ к отдельному символу одного из аргументов командной строки, введите в argv второй индекс. Например, следующая программа посимвольно выводит все аргументы, с которыми ее вызвали: #include int main(int argc, char *argv[]) { int t, i; for(t=0; t #include void pr_reverse(char *s); int main(void) { pr_reverse("Мне нравится C"); return 0; } void pr_reverse(char *s) { register int t; for(t=strlen(s)-1; t>=0; t--) putchar(s[t]); } Как только строка выведена на экран, функции pr_reverse() "делать больше нечего", поэтому она возвращает управление туда, откуда она была вызвана. Но на практике не так уж много функций используют именно такой способ завершения выполнения. В большинстве функций для завершения выполнения используется оператор return — или потому, что необходимо вернуть значение, или чтобы сделать код функции проще и эффективнее. В функции может быть несколько операторов return. Например, в следующей программе функция find_substr() возвращает начальную позицию подстроки в строке или же возвращает —1, если подстрока, наоборот, не найдена. В этой функции для упрощения кодирования используются два оператора return. #include int find_substr(char *s1, char *s2); int main(void) { if(find_substr("C - это забавно", "is") != -1) printf("Подстрока найдена."); return 0; } /* Вернуть позицию первого, вхождения s2 в s1. */ int find_substr(char *s1, char *s2) { register int t; char *p, *p2; for(t=0; s1[t]; t++) { p = &s1[t]; p2 = s2; while(*p2 && *p2==*p) { p++; p2++; } if(!*p2) return t; /* 1-й оператор return */ } return -1; /* 2-й оператор return */ } Возврат значений Все функции, кроме тех, которые относятся к типу void, возвращают значение. Это значение указывается выражением в операторе return. Стандарт С89 допускает выполнение оператора return без указания выражения внутри функции, тип которой отличен от void. В этом случае все равно происходит возврат какого-нибудь произвольного значения. Но такое положение дел, мягко говоря, никуда не годится! Поэтому в Стандарте С99 (да и в C++) предусмотрено, что в функции, тип которой отличен от void, в операторе return необходимо обязательно указать возвращаемое значение. То есть, согласно С99, если для какой-либо функции указано, что она возвращает значение, то внутри этой функции у любого оператора return должно быть свое выражение. Однако если функция, тип которой отличен от void, выполняется до самого конца (то есть до закрывающей ее фигурной скобки), то возвращается произвольное (непредсказуемое с точки зрения разработчика программы!) значение. Хотя здесь нет синтаксической ошибки, это является серьезным упущением и таких ситуаций необходимо избегать. Если функция не объявлена как имеющая тип void, она может использоваться как операнд в выражении. Поэтому каждое из следующих выражений является правильным: x = power(y); if(max(x,y) > 100) printf("больше"); for(ch=getchar(); isdigit(ch); ) ... ; Общепринятое правило гласит, что вызов функции не может находиться в левой части оператора присваивания. Выражение swap(x,y) = 100; /* неправильное выражение */ является неправильным. Если компилятор С в какой-либо программе найдет такое выражение, то пометит его как ошибочное и программу компилировать не будет. В программе можно использовать функции трех видов. Первый вид — простые вычисления. Эти функции предназначены для выполнения операций над своими аргументами и возвращают полученное в результате этих операций значение. Вычислительная функция является функцией "в чистом виде". В качестве примеров можно назвать стандартные библиотечные функции sqrt() и sin(), которые вычисляют квадратный корень и синус своего аргумента соответственно. Второй вид включает в себя функции, которые обрабатывают информацию и возвращают значение, которое показывает, успешно ли была выполнена эта обработка. Примером является библиотечная функция fclose(), которая закрывает файл. Если операция закрытия была завершена успешно, функция возвращает 0, а в случае ошибки она возвращает EOF. У функций последнего, третьего вида нет явно возвращаемых значений. В сущности, такие функции являются чисто процедурными и никаких значений выдавать не должны. Примером является exit(), которая прекращает выполнение программы. Все функции, которые не возвращают значение, должны объявляться как возвращающие значение типа void. Объявляя функцию как возвращающую значение типа void, вы запрещаете ее применение в выражениях, предотвращая таким образом случайное использование этой функции не по назначению. Иногда функции, которые, казалось бы, фактически не выдают содержательный результат, все же возвращают какое-то значение. Например, printf() возвращает количество выведенных символов. Если бы нашлась такая программа, которая на самом деле проверяла бы это значение, то это было бы что-то необычное... Другими словами, хотя все функции, за исключением относящихся к типу void, возвращают значения, вовсе не нужно стремиться использовать эти значения во что бы то ни стало. Часто при обсуждении значений, возвращаемых функциями, возникает такой довольно распространенный вопрос: "Неужели не обязательно присваивать возвращенное значение какой-либо переменной? Не повиснет ли оно где-нибудь и не приведет ли это в дальнейшем к каким-либо неприятностям?" Отвечая на этот вопрос, повторим, что присваивание отнюдь не является обязательным, причем отсутствие его не станет причиной каких-либо неприятностей. Если возвращаемое значение не входит ни в один из операторов присваивания, то это значение будет просто отброшено. Такое отбрасывание значения встречается очень часто. #include int mul(int a, int b); int main(void) { int x, y, z; x = 10; y = 20; z = mul(x, y); /* 1 */ printf("%d", mul(x,y)); /* 2 */ mul(x, y); /* 3 */ return 0; } int mul(int a, int b) { return a*b; } В строке 1 значение, возвращаемое функцией mul(), присваивается переменной z. В строке 2 возвращаемое значение не присваивается, но используется функцией printf(). И наконец, в строке 3 возвращаемое значение теряется, потому что не присваивается никакой из переменных и не используется как часть какого-либо выражения. Возвращаемые указатели Хотя с функциями, которые возвращают указатели, обращаются так же, как и с любыми другими функциями, все же будет полезно познакомиться с некоторыми основными понятиями и рассмотреть соответствующий пример. Указатели не являются ни целыми, ни целыми без знака. Они являются адресами в памяти и относятся к особому типу данных. Такая особенность указателей определяется тем, что арифметика указателей (адресная арифметика) работает с учетом параметров базового типа. Например, если указателю на целое придать минимальное (ненулевое) приращение, то его текущее значение станет на четыре больше, чем предыдущее (при условии, что целые значения занимают 4 байта). Вообще говоря, каждый раз, когда значение указателя увеличивается (уменьшается) на минимальную величину, то он указывает на последующий (предыдущий) элемент, имеющий базовый тип указателя. Так как размеры разных типов данных могут быть разными, то компилятор должен знать тип данных, на которые может указывать указатель. Поэтому в объявлении функции, которая возвращает указатель, тип возвращаемого указателя должен декларироваться явно. Например, нельзя объявлять возвращаемый тип как int *, если возвращается указатель типа char *! Иногда (правда, крайне редко!) требуется, чтобы функция возвращала "универсальный" указатель, т.е. указатель, который может указывать на данные любого типа. Тогда тип результата функции следует определить как void *. Чтобы функция могла возвратить указатель, она должна быть объявлена как возвращающая указатель на нужный тип. Например, следующая функция возвращает указатель на первое вхождение символа, присвоенного переменной с, в строку s. Если этого символа в строке нет, то возвращается указатель на символ конца строки ('0'). /* Возвращает указатель на первое вхождение c в s. */ char *match(char c, char *s) { while(c!=*s && *s) s++; return(s); } Вот небольшая программа, в которой используется функция match(): #include char *match(char c, char *s); /* прототип */ int main(void) { char s[80], *p, ch; gets(s); ch = getchar(); p = match(ch, s); if(*p) /* символ найден */ printf("%s ", p); else printf("Символа нет."); return 0; } Эта программа сначала считывает строку, а затем символ. Потом проводится поиск местонахождения символа в строке. При наличии символа в строке переменная p укажет на него, и программа выведет строку, начиная с найденного символа. Если символ в строке не найден, то p укажет на символ конца строки ( '0' ), причем *p будет представлять логическое значение ЛОЖЬ (false). В таком случае программа выведет сообщение Символа нет. Функция типа void Одним из применений ключевого слова void является явное объявление функций, которые не возвращают значений. Мы уже знаем, что такие функции не могут применяться в выражениях, и указание ключевого слова void предотвращает их случайное использование не по назначению. Например, функция print_vertical() выводит в боковой части экрана свой строчный аргумент по вертикали сверху вниз. void print_vertical(char *str) { while(*str) printf("%c\n", *str++); } Вот пример использования функции print_vertical(): #include void print_vertical(char *str); /* прототип */ int main(int argc, char *argv[]) { if(argc > 1) print_vertical(argv[1]); return 0; } void print_vertical(char *str) { while(*str) printf("%c\n", *str++); } И еще одно замечание: в ранних версиях С ключевое слово void не определялось. Таким образом, в программах, написанных на этих версиях С, функции, которые не возвращали значений, просто имели по умолчанию тип int — и это несмотря на то, что они не возвращали никаких значений! Возвращаемое значение функции main() Функция main() возвращает целое число, которое принимает вызывающий процесс — обычно этим процессом является операционная система. Возврат значения из main() эквивалентен вызову функции exit() с тем же самым значением. Если main() нe возвращает значение явно, то вызывающий процесс получает формально неопределенное значение. На практике же большинство компиляторов С автоматически возвращают 0, но если встает вопрос переносимости, то на такой результат полагаться с уверенностью нельзя. Рекурсия В языке С функция может вызывать сама себя. В этом случае такая функция называется рекурсивной. Рекурсия — это процесс определения чего-либо на основе самого себя, из-за чего рекурсию еще называют рекурсивным определением. Простым примером рекурсивной функции является factr(), которая вычисляет факториал целого неотрицательного числа. Факториалом числа n (обозначается n!) называется произведение всех целых чисел, от 1 до n включительно (для 0, по определению, факториал равен 1.). Например, 3! — это 1×2×3, или 6. Здесь показаны factr() и эквивалентная ей функция, в которой используется итерация: /* рекурсивная функция */ int factr(int n) { int answer; if(n==1) return(1); answer = factr(n-1)*n; /* рекурсивный вызов */ return(answer); } /* неркурсивная функция */ int fact(int n) { int t, answer; answer = 1; for(t=1; t<=n; t++) answer=answer*(t); return(answer); } Нерекурсивное вычисление факториала, то есть вычисление с помощью fact(), выполняется достаточно просто. В этой функции в теле цикла, выполняющемся для t от 1 до n, вычисленное ранее произведение последовательно умножается на каждое из этих чисел. (Значение факториала для 0 получается, конечно, с помощью оператора присваивания. Значение факториала для 1 также получается умножением не на ранее полученное произведение, а на заранее подготовленное число, тоже равное 1.) Работа же рекурсивной функции factr() чуть более сложная. Когда factr() вызывается с аргументом 0, то она сразу возвращает 1. Если же аргумент больше 0, то возвращается произведение factr(n-1)*n. Чтобы вычислить значение этого выражения, factr() вызывается с аргументом n-1. Это выполняется до тех пор, пока n не станет равным 0. Когда это произойдет, вызовы функции начнут возвращать вычисленные ими значения факториалов. При вычислении 2! первый вызов factr() влечет за собой второй, теперь уже рекурсивный вызов с аргументом 1, который, в свою очередь, влечет третий, тоже рекурсивный вызов с аргументом 0. Этот вызов возвращает число 1, которое затем умножается на 1, а потом на 2 (первоначальное значение n). Ответ в данном случае равен 2. Попробуйте самостоятельно вычислить 3!. (Вам, возможно, захочется вставить в функцию factr() выражения printf(), чтобы видеть уровень каждого вывода, и то, какие будут промежуточные ответы.) Когда функция вызывает сама себя, новый набор локальных переменных и параметров размещается в памяти в стеке, а код функции выполняется с самого своего начала, причем используются именно эти новые переменные. При рекурсивном вызове функции новая копия ее кода не создается. Новыми являются только значения, которые использует данная функция. При каждом возвращении из рекурсивного вызова старые локальные переменные и параметры извлекаются из стека, и сразу за рекурсивным вызовом возобновляется работа функции. При использовании рекурсивных функций стек работает подобно "телескопической" трубе, выдвигающейся вперед и складывающейся обратно. Хотя и кажется, что рекурсия предлагает более высокую эффективность, но на самом деле такое бывает достаточно редко. Использование рекурсии в программах зачастую не очень сильно уменьшают их размер кода и обычно только незначительно увеличивает эффективность использования памяти. Кроме того, рекурсивные версии большинства программ могут выполняться несколько медленнее, чем их итеративные варианты, потому что при рекурсивных вызовах функций расходуются дополнительные ресурсы. Кроме того, большое количество рекурсивных вызовов функции может вызвать переполнение стека. Из-за того, что память для параметров функции и локальных переменных находится в стеке и при каждом новом вызове создается еще один набор этих переменных, то для переменных места в стеке может рано или поздно не хватить. Переполнение стека — вот обычная причина аварийного завершения программы, когда функция утрачивает контроль над рекурсивными обращениями. Главным преимуществом рекурсивных функций является то, что с их помощью упрощается реализация некоторых алгоритмов, а программа становится понятнее. Например, алгоритм быстрой сортировки трудно реализовать итеративным способом. Кроме того, для некоторых проблем, особенно связанных с искусственным интеллектом, больше подходят рекурсивные решения. И наконец, некоторым людям легче думать рекурсивными категориями, чем итеративными. В тексте рекурсивной функции обязательно должен быть выполнен условный оператор, например if, который при определенных условиях вызовет завершение функции, т.е. возврат, а не выполнит очередной рекурсивный вызов. Если такого оператора нет, то после вызова функция никогда не сможет завершить работы. Распространенной ошибкой при написании рекурсивных функций как раз и является отсутствие в них условного оператора. При создании программ не отказывайтесь от функции printf(); тогда вы сможете увидеть, что происходит на самом деле и сможете прервать выполнение, когда обнаружите ошибку. Прототип функции В современных, правильно написанных программах на языке С каждую функцию перед использованием необходимо объявлять. Обычно это делается с помощью прототипа функции. В первоначальном варианте языка С прототипов не было; но они были введены уже в Стандарт С89. Хотя прототипы формально не требуются, но их использование очень желательно. (Впрочем, в C++ прототипы обязательны. Прототипы дают компилятору возможность тщательнее выполнять проверку типов, подобно тому, как это делается в таких языках как Pascal. Если используются прототипы, то компилятор может обнаружить любые сомнительные преобразования типов аргументов, необходимые при вызове функции, если тип ее параметров отличается от типов аргументов. При этом будут выданы предупреждения обо всех таких сомнительных преобразованиях. Компилятор также обнаружит различия в количестве аргументов, использованных при вызове функции, и в количестве параметров функции. В общем виде прототип функции должен выглядеть таким образом: тип имя_функции(тип имя_парам1, тип имя_парам2, ..., имя_парамN); Использование имен параметров не обязательно. Однако они дают возможность компилятору при наличии ошибки указать имена, для которых обнаружено несоответствие типов, так что не поленитесь указать этих имен — это позволит сэкономить время впоследствии. Следующая программа показывает, насколько ценными являются прототипы функций. В ней выводится сообщение об ошибке, происходящей из-за того, что программа содержит попытку вызова sqr_it() с целым аргументом, в то время как требуется указатель на целое. /* В этой программе используется прототип функции чтобы обеспечить тщательную проверку типов. */ void sqr_it(int *i); /* прототип */ int main(void) { int x; x = 10; sqr_it(x); /* несоответствие типов */ return 0; } void sqr_it(int *i) { *i = *i * *i; } В качестве прототипа функции может также служить ее определение, если оно находится в программе до первого вызова этой функции. Вот, например, правильная программа: #include /* Это определение будет также служить и прототипом внутри этой программы. */ void f(int a, int b) { printf("%d ", a % b); } int main(void) { f(10,3); return 0; } В этом примере специальный прототип не требуется; так как функция f() определена еще до того, как она начинает использоваться в main(). Хотя определение функции и может служить ее прототипом в малых программах, но в больших такое встречается редко — особенно, когда используется несколько файлов. Единственная функция, для которой не требуется прототип — это main(), так как это первая функция, вызываемая в начале работы программы. Имеется небольшая, но важная разница в том, как именно в С и C++ обрабатывается прототип функции, не имеющей параметров. В C++ пустой список параметров указывается полным отсутствием в прототипе любых параметров. Например, int f(); /* Прототип C++ для функции, не имеющей параметров */ Однако в С это выражение означает нечто другое. Из-за необходимости придерживаться совместимости с первоначальной версией С пустой список параметров сообщает, что просто о параметрах не предоставлено никакой информации. Что касается компилятора, то для него эта функция может иметь несколько параметров, а может не иметь ни одного. (Такой оператор называется Устаревшим объявлением функции. Если функция в языке С не имеет параметров, то в ее прототипе внутри списка параметров стоит только ключевое слово void. Вот, например, прототип функции f() в том виде, в каком он должен быть в программе на языке С: float f(void); Таким образом компилятор узнает, что у функции нет параметров, и любое обращение к ней, в котором имеются аргументы, будет считаться ошибкой. В C++ использование ключевого слова void внутри пустого списка параметров также разрешено, но считается излишним. Прототипы функций позволяют "отлавливать" ошибки еще до запуска программы. Кроме того, они запрещают вызов функций при несовпадении типов (т.е. с неподходящими аргументами) и тем самым помогают проверять правильность программы. И напоследок хотелось бы сказать следующее: так как в ранних версиях С синтаксис прототипов в полном объеме не поддерживался, то в С прототипы формально не обязательны. Такой подход необходим для совместимости с С-кодом, созданным еще до появления прототипов. Но если старый С-код переносится в C++, то перед компиляцией этого кода в него необходимо добавить полные прототипы функций. Помните, что хотя прототипы в С не обязательны, но они обязательны в C++. Это значит, что каждая функция в программе на языке C++ должна иметь полный прототип. Поэтому при написании программ на С в них указываются полные прототипы функций — именно так поступает большинство программистов, работающих на этом языке. Устаревшие объявления функций В "ранней молодости" языка С, еще до создания прототипов функций, все-таки была необходимость сообщить компилятору о типе результата функции, чтобы при вызове функции был создан правильный код. (Так как размеры разных типов данных разные, то размер типа результата надо было знать еще до вызова функции.) Это выполнялось с помощью объявления функции, не содержащего никакой информации о параметрах. С точки зрения теперешних стандартов этот Устаревший подход является архаичным. Однако его до сих пор можно найти в старых кодах. По этой причине важно понимать, как он работает. Согласно Устаревшему подходу, тип результата и имя функции объявляются почти что в начале программы: #include double div(); /* Устаревшее объявление функции */ int main(void) { printf("%f", div(10.2, 20.0)); return 0; } double div(double num, double denom) { return num / denom; } Устаревшее объявление типа функции сообщает компилятору, что функция div() возвращает результат типа double. Это объявление позволяет компилятору правильно генерировать код для вызовов этой функции. Однако оно ничего не говорит о параметрах div(). Общий вид Устаревшего оператора объявления функции такой: спецификатор_типа имя_функции(); !!!Список параметров пустой. Даже если функция принимает аргументы, то ни один из них не перечисляется в объявлении типа. Устаревшее объявление функции устарело и не должно использоваться в новом коде. Кроме того, оно несовместимо с C++. Прототипы устаревших библиотечных функций Любая стандартная библиотечная функция в программе должна иметь прототип. Поэтому для каждой такой функции необходимо ввести соответствующий заголовок. Все необходимые заголовки предоставляются компилятором С. В системе программирования на языке С библиотечными заголовками (обычно) являются файлы, в именах которых используется расширение .h. В заголовке имеется два основных элемента: любые определения, используемые библиотечными функциями, и прототипы библиотечных функций. Объявление списков параметров переменной длины Можно вызвать функцию, которая имеет переменное количество параметров. Самым известным примером является printf(). Чтобы сообщить компилятору, что функции будет передано заранее неизвестное количество аргументов, объявление списка ее параметров необходимо закончить многоточием. Например, следующий прото тип указывает, что у функции func() будет как минимум два целых параметра и после них еще некоторое количество (в том числе и 0) параметров: int func(int a, int b, ...); В любой функции, использующей переменное количество параметров, должен быть как минимум один реально существующий параметр. Например, следующее объявление неправильное: int func(...); /* ошибка */ Правило "неявного int" Первоначальная версия С отличалась особенностью, которую иногда называют правилом "неявного int" (а также правилом "int по умолчанию"). Это правило состоит в том, что если спецификатор базового типа явно не указан, то подразумевается спецификатор int. Это правило было включено в стандарт С89, но в С99 это правило не вошло. Больше всего правило "неявного int" использовалось при определении типа результата функции, т.е. при определении возвращаемого типа. Много лет назад большинство программистов, писавших программы на С, пользовались этим правилом, когда писали код функций, возвращавших результат типа int. Поэтому много лет назад такая функция, как int f(void) { /* ... */ return 0; } часто могла быть написана таким образом: f(void) { /* int - возвращаемый тип по умолчанию */ /* ... */ return 0; } В первом случае возвращаемый тип int определяется явно. Во втором же — подразумевается по умолчанию. Правило "неявного int" применяется не только к значениям, возвращаемым функциями (хотя это было самое распространенное применение). Например, для С89 и более ранних вариантов С правильной является следующая функция: /* по умолчанию возвращается тип int, такой же тип, как и у параметров a и b */ f(register a, register b) { register c; /* переменная c по умолчанию также будет иметь тип int */ c = a + b; printf("%d", c); return c; } Здесь возвращаемым типом по умолчанию для f() является int; т.е. такой же тип по умолчанию, как и у параметров а и b, и у локальной переменной c. Помните, что правило "неявного int" не поддерживается в С99 или C++. Таким образом, использовать его в программах, совместимых с С89, не рекомендуется. Лучше всего явно определять каждый тип, используемый в вашей программе. Устаревшие и современные объявления параметров функций В ранних версиях С использовался синтаксис объявления параметров, отличающийся от того, который используется в современных версиях этого языка, включая С89, С99 и C++. Такой ранний синтаксис иногда называется классическим. В стандартном С поддерживаются оба синтаксиса, но настоятельно рекомендуется современный. (А в C++ поддерживается только современный синтаксис объявления параметров.) Однако Устаревший синтаксис надо знать, потому что он до сих пор используется во многих старых программах, написанных на языке С. Устаревшее объявление параметров функции состоит из двух частей: списка параметров внутри круглых скобок, следующих за именем функции, а также объявлений параметров, находящихся между закрывающей круглой скобкой и открывающей фигурной скобкой функции. В общем виде Устаревшее определение параметров должно выглядеть таким образом: тип имя_функции(парм1, парм2,...пармN) тип парм1; тип парм2; . . . тип пармN; { код функции } Например, такое современное объявление, как float f(int a, int b, char ch) { /* ... */ } в Устаревшем виде будет выглядеть следующим образом: float f(a, b, ch) int a, b; char ch; { /* ... */ } Устаревший синтаксис позволяет в списке, стоящем за именем типа, объявить более одного параметра. Устаревший синтаксис объявления параметров признан устаревшим для стандартного С и не поддерживается в языке C++. Ключевое слово inline В Стандарте С99 было введено ключевое слово inline, применяемое к функциям. Оно подробно рассматривается в части II, а здесь мы дадим только его краткое описание. Ставя перед объявлением функции ключевое слово inline, вы даете компилятору указание оптимизировать вызовы этой функции. Обычно это означает, что такие вызовы желательно заменить последовательной вставкой кода самой функции. Однако inline является всего лишь запросом к компилятору и может быть проигнорировано. Перегрузка имен функций Язык «С» позволяет использовать одно и тоже имя для нескольких различных функций. Эта возможность называется перегрузкой имен функций. Распознавание таких функций осуществляется посредством передаваемых параметров: их типов и порядком передачи. ВАЖНО: Распознавание по типу возвращаемого значения – не производится. Рассмотрим пример нескольких таких функций: double ABS(double); int ABS(int); double func(double, int); double func(int, double); void main() { double x=10.0,y=-15.0; int a=5, b=-20; printf("%.2lf\t%.2lf\n",ABS(x),ABS(y)); printf("%d\t%d\n",ABS(a),ABS(b)); printf("%.2lf\t%.2lf\n",func(x,a),func(b,y)); } double ABS(double v) {return fabs(v);} int ABS(int v) {return abs(v);} double func(double x, int a) {return pow(x,(double)a);} double func(int a, double x) {return x*a;} Классы памяти При использовании многофункциональной структуры программы возникают такие вопросы как видимость и время жизни переменных. Под видимостью переменной понимают возможность использования одной и той же переменной в различных функциях. В рамках этого понятия выделяют два вида переменных (данных): локальные и глобальные. Локальные переменные – переменные видимые только внутри одной функции, а глобальные переменные – переменные видимые внутри нескольких функций. В языке «С» локальные переменные описываются внутри функций: в начале тела функции с помощью операторов объявлений, а глобальные переменные описываются вне функций. Для типов данных и констант все аналогично переменным. Рассмотрим пример программы: int Arr[10]; void InArr(); void SortArr(); void OutArr(); void main(){InArr(); SortArr(); OutArr();} void InArr(){ int i; printf("Input array:\n"); for(i=0;i<10;i++) scanf("%d",&Arr[i]); } void SortArr() { int i,f=1,r; while(f){ f=0; for(i=0;i<9;i++) if(Arr[i]>Arr[i+1]){ r=Arr[i];Arr[i]=Arr[i+1];Arr[i+1]=r;f=1; }}} void OutArr(){ int i; printf("Output array:\n"); for(i=0;i<10;i++) printf("%3d ",Arr[i]); } В программе объявлен глобальный массив Arr. В функции main содержаться только вызовы дочерних функций. Функция InArr осуществляет ввод массива. В ней описана локальная переменная i – счетчик цикла. Функция SortArr осуществляет сортировку массива, в ней описаны три локальные переменные i, f, r – счетчик цикла, признак завершения сортировки и вспомогательная переменная для сортировки соответственно. Функция OutArr осуществляет вывод массива на экран. Данную программу можно модифицировать, перенеся объявление переменной Arr после функции main и вынеся переменную i перед функциями InArr, SortArr, OutArr. При объявлении переменных можно использовать одно и тоже имя для локальных переменных в разных функциях, как это было сделано в предыдущем примере, а можно использовать одно и тоже имя для локальной и глобальной переменной, но в таком случае использоваться будет локальная переменная. Рассмотрим пример: int a=5; void Show(void); void main(){int a=6; printf("%d\n",a); Show();} void Show(void){printf("%d\n",a);} При выполнении данной программы на экран будет выведено: 6 5. В функции main использовалась локальная переменная, а в функции Show – глобальная. Подводя итог можно привести два правила: Локальные переменные описываются внутри функций и видны только внутри них. Глобальные переменные описываются вне функций и видны во всех функциях, которые описаны после этих переменных. Понятие времени жизни переменной определяет период, в который переменной выделена память и она содержит значение. Так глобальные переменные создаются в сегменте данных программы и живут все время выполнения программы. Локальные переменные располагаются в стеке, и существуют, пока выполняется соответствующая функция (но возможны и исключения). В языке «С» реализованы четыре класса памяти: auto, register, static и extern. Для задания класса памяти переменной необходимо указать его при объявлении: класс_памяти тип имя_переменной; Все это время мы работали с классом памяти auto, который используются по умолчанию. Класс register указывает компилятору на то, что данную переменную необходимо по возможности хранить в регистре (индексные переменные). Класс static указывает на то, что переменная должна существовать на всем протяжении выполнения программы. Класс extern указывает на то, что переменная объявлена где-то в другом месте (используется при многомодульном программировании, и будет рассматриваться позднее). Рассмотрим пример программы: void Show1(void); void Show2(void); void main(){Show1(); Show1(); Show2(); Show2();} void Show1(void){int a=1; printf("%d\n",a); a++;} void Show2(void){static int a=1; printf("%d\n",a); a++;} При выполнении данной программы на экран будет выведено: 1 1 1 2. При выполнении функции Show1 переменная a создается каждый раз и инициализируется значением «1». При выполнении функции Show2 локальная переменная a создается и инициализируется только один раз. При втором вызове данная переменная уже существует. Все глобальные переменные по умолчанию являются статическими и существуют на протяжении выполнения всей программы. Созданием переменных управляет компилятор языка «С» и он может оптимизировать этот процесс. Поэтому наиболее часто переменные создаются в момент их первого использования. В режиме отладки при просмотре переменной, которая еще не была создана, выводится сообщение: variable was optimized. Рекурсивные алгоритмы: понятие, глубина рекурсии, рекурсивный спуск и подъем, граничное условие. Правила организации рекурсивных алгоритмов Рекурсивные алгоритмы. Основные понятия Слово «рекурсивный» и «рекуррентный» имеют общий смысл, они происходят от латинского reccuro – бежать назад, возвращаться. Рекурсия — это такой способ организации подпрограммы, при котором в ходе выполнения операторов подпрограммы встречается обращение (оператор вызова) подпрограммы самой к себе. Способ сведения задачи к ней же самой, но с измененными входными данными, называется рекурсией. Алгоритм решения задачи, который содержит обращения к себе, называется рекурсивным алгоритмом. Например: Алгоритм Подпр; Вход: список_имен_переменых:тип; Выход: : список_имен_переменых:тип; Начало Оператор 1; Оператор 2; … Оператор n; Подпр(вход, выход); Оператор n+1; … Оператор n+m; Конец; Алгоритм программы; Начало Подпр(вход,выход); Конец. Рекурсивные шаги Фрейм стека рекурсивной функции При вызове подпрограммы Подпр(вход,выход) в стеке размещается запись активации (фрейм стека) подпрограммы, содержащая сведения о фактических параметрах (в порядке, обратном их следованию в списке), локальных переменных подпрограммы, и адресе возврата (для корректного возврата из подпрограммы), а для функций – еще и значение функции. После выполнения оператора подпрограммы Оператор n следует рекурсивный шаг, то есть активация (вызов) подпрограммы Подпр(вход,выход). В сегменте стека создается вторая копия записей активации подпрограммы. Важным свойством рекурсии является тот факт, что при рекурсивном вызове функции ее первый вызов еще не закончился, то есть операторы от (n+1)-го до (n+m) – го при этом еще не выполнены. В стеке запоминается адрес возврата - Оператор n+1 – для данного рекурсивного шага. Выполнение следующих рекурсивных шагов опять таки приводит к выполнению операторов 1.. n, оператора вызова подпрограммы и порождению новых записей активации в программном стеке. В стеке размещаются последовательности фреймов. Программа в каждый конкретный момент работает с последним вызовом и с последним фреймом. При завершении рекурсии программа возвращается к предыдущей версии рекурсивной функции и к предыдущему фрейму в стеке. Таким образом, что все рекурсивные вызовы являются идентичными, а отличаются только начальным состоянием, текущими параметрами и точкой возврата. Очевидно, что рекурсия не может быть безусловной, в этом случае она становится бесконечной. Рекурсия должна иметь внутри себя условие завершения, по которому очередной рекурсивный вызов уже не производится. Такое условие называется граничным. При этом фактические параметры подпрограммы должны меняться таким образом, чтобы это условие выполнялось за конечное число рекурсивных шагов. Глубина рекурсии, конечный шаг, рекурсивный спуск и подъем Оператор, ассоциированный с условием, выполнение которого является признаком завершения рекурсии, называется конечным шагом. Когда условие завершения становиться истинно, рекурсивная подпрограмма приступает к выполнению конечного шага. При этом выходные параметры программы должны принимать определенные значения. Количество рекурсивных шагов от первого до конечного называется глубиной рекурсии. Последовательность рекурсивных вызовов подпрограммы до выхода на граничное условие называется рекурсивным спуском. Завершение работы рекурсивных подпрограмм называется рекурсивным подъемом. На этапе рекурсивного подъема, память в программном стеке, выделенная для записей активации подпрограмм, освобождается в порядке, обратном ее заполнению. На данном этапе происходит вычислительный процесс в соответствии с алгоритмом подпрограммы. Трассировка рекурсивной функции Типичным примером применения механизма рекурсии служит задача вычисления значения факториала (n!). Факториал числа N, представляющий собой произведение всех положительных целых чисел, меньших или равных N, обозначают в математике как N!: . 0!=1; n!=n*(n-1)! 5!=5*4!=5*4*3!= 5*4*3*2!= 5*4*3*2*1!= 5*4*3*2*1=120 {циклический алгоритм} {рекурсивный алгоритм} {рекурсивный алгоритм} long fact Начало Fact:=1; For i:=n downto 1 do Fact:=Fact*I; Конец; Начало If n=0 то RecFactorial:=1 иначе Fact:=RecFactorial(n-1)*n Конец; Начало If n=0 то RecFactorial:=1 иначе Fact:= n *RecFactorial(n-1) Конец; НАЧАЛО F:=Factor(5); Вывод(F); Ввод; КОНЕЦ. НАЧАЛО F:=RecFactorial(5); Вывод(F); Ввод; КОНЕЦ. НАЧАЛО F:=RecFactorial(5); Вывод(F); Ввод; КОНЕЦ. Виды рекурсии Явной (линейной или авторекурсией) рекурсией называется способ организации подпрограммы, при котором в разделе операторов этой подпрограммы встречается единственный условный вызов самой себя. В таком случае рекурсия становится эквивалентной обычному циклу. Действительно, любой циклический алгоритм можно преобразовать в линейно-рекурсивный и наоборот. Неявной (косвенной или взаимной) рекурсией называется способ организации подпрограмм, при котором в теле операторов одной подпрограммы встречается оператор вызова второй подпрограммы, в теле второй подпрограммы – оператор вызова первой подпрограммы. Например, процедура A вызывает процедуру B, а та, в свою очередь, процедуру A. По тексту программы рекурсивность косвенно рекурсивных подпрограмм определена не явно. С точки зрения практической реализации косвенная рекурсии допустима в тех языках программирования, которые поддерживают механизм рекурсии. Для этого необходимо, чтобы формальные параметры процедуры передавались через стек. Немаловажно также, чтобы временные объекты, создаваемые внутри процедуры в ходе ее выполнения, тоже располагались в стеке. Свойства рекурсивных задач и решений Применение рекурсии обосновано в случаях: 1) работы с рекурсивными структурами данных. Рекурсивная структура данных – такая структура данных, в которой элемент структуры данных содержит один или несколько указателей на такую же структуру данных. Например, односвязный список можно определить как элемент списка, содержащий указатель NULL или указатель на аналогичный список; 2) при реализации процесса, имеющего рекурсивную природу (например, в таких, как factorial и nod). Многие математические функции определяются рекурсивно. В остальных случаях, то есть при обработке не существенно рекурсивных по своей природе структур данных и процессов, целесообразна четкая оценка быстродействия и надежности разрабатываемых подпрограмм. В общем случае наилучшее применение рекурсии – это решение задач, для которых свойственна одна черта: решение задачи в целом сводится к решению подобной же задачи, но меньшей размерности и, следовательно, легче решаемой, то есть задач, основанных на идее возвратности (или рекуррентности), в математике – на методе математической индукции. Этот метод используется для доказательства корректности утверждений для бесконечной последовательности состояний, а именно: если утверждение верно в начальном состоянии, а из его справедливости в n-м состоянии можно доказать его справедливость в n+1-м состоянии, то такое утверждение будет справедливым всегда. Задачи, которые могут быть решены с помощью рекурсии, имеют следующие характеристики 1. в задаче можно выделить один или больше конечных шагов, имеющих простое, не рекурсивное решение. 2. все прочие шаги задачи могут быть разделены (с использованием рекурсии) на задачи, которые приближаются к конечным шагам. В конце концов, вся задача может быть сведена таким образом только к конечным шагам, имеющим сравнительно простое решение. Для решения рекурсивной задачи следует придерживаться такой последовательности действий: • определить в задаче конечные шаги • определить в задаче рекурсивные шаги. Другими словами, необходимо вывести рекуррентное соотношение для решаемой задачи. Совокупность равенств, в которых заданы начальные значения и зависимость общего члена от предыдущего, называется рекуррентностью (возвратным соотношением или рекурсивной зависимостью). (Грэхем, Пташник, Кнут. Конкретная математика) Соотношения, связывающие одни и те же функции, но с различными аргументами, называются рекуррентными соотношениями или рекуррентными уравнениями. Соотношения должны быть определены для всех допустимых значений аргументов. Поэтому должны быть определены значения функций при начальных значениях параметров. Правильными рекуррентными соотношениями (уравнениями) называют такие рекуррентные соотношения, у которых количество или значения аргументов у функций в правой части соотношения меньше количества или, соответственно, значений аргументов функции в левой части соотношения. Если аргументов несколько, то достаточно уменьшения одного из аргументов. Правила организаци рекурсивных алгоритмов Типичной ошибкой организации рекурсивных алгоритмов является отсутствие корректного или полного условия ее завершения. Поэтому для рекурсивных пподпрограмм весьма важно, чтобы на каком-то уровне вложенности дальнейшие вызовы прекращались, в противном случае произойдет зацикливание до тех пор, пока пространство стека не будет исчерпано. При разработке рекурсивных подпрограмм необходимо придерживаться следующих правил: 1. необходимо обеспечить завершение процесса рекурсивных вызовов подпрограммы на определенном шаге. Для этого: a. в теле функции должно быть предусмотрено условие, при выполнении которого дальнейшая рекурсия не требуется; b. необходимо обеспечить такое изменение данных при повторных вызовах подпрограмм, чтобы обеспечить наступление этого условия; 2. глубина рекурсии, т.е. максимальное количество вложенных вызовов функции или процедуры, должна быть не слишком большой; 3. необходимо экономно расходовать память под локальные переменные рекурсивной подпрограммы; 4. не следует отключать контроль использования стека (опция меню Options/Compiler/Stack checking или директива компилятора {$S+}) во избежание его переполнения. Переполнение стека может вызвать сбой системы и повлечь за собой перезагрузку системы; 5. использование в разделе операторов рекурсивной подпрограммы (как и в любых других подпрограммах) глобальных переменных недопустимо Технология решения простых рекурсивных задач Рекурсивная функция представляет собой переход из n-го в n+1 состояние некоторого процесса. Если этот переход корректен, то есть соблюдение некоторых условий на входе функции приводит к их соблюдению на выходе, (то есть в рекурсивном вызове), то эти условия будут соблюдаться во всей цепочке состояний (при безусловной корректности начального). Отсюда следует, что самое важное в определении рекурсии - выделить те условия, которые соблюдаются во всех точках процесса и обеспечить их справедливость от входа в рекурсивную функцию до ее рекурсивного вызова. Если рассматривать простые (классические) задачи, то для каждого витка рекурсии схема рекурсивных вычислений одинакова. Для задач данного типа можно определить технологию решения схематично следующим образом: Реализация рекурсивных алгоритмов с помощью функций, заданных рекуррентным соотношением: Procedure F(<список_параметров>:<тип_параметров>); Формальные параметры рекурсивной функции представляют обобщенные начальные данные для текущего рекурсивного вызова подпрограммы Var <список_локальных_переменных>:<тип_переменных>; локальными переменными функции должны быть объявлены все переменные, которые имеют отношение к протеканию текущего шага процесса. начало If n=<значение_1> Условие выхода из рекурсивного алгоритма, т.е. условие при котором очередной рекурсивный вызов уже не производится. При этом должны быть определены значения выходных параметров то<значение_функции_1> иначе If то <значение_функции_2> иначе Фактические параметры рекурсивного вызова представляют начальное состояние для следующего рекурсивного вызова if то F(<список_параметров_1>) иначе F(<список_параметров_2>); рекурсивные вызовы с измененными параметрами функцииы} конец; Начало {Инициализация начальных значений списка фактических параметров рекурсивной подпрограммы} F(<список_фактических_параметров>); Конец. Реализация рекурсивных алгоритмом с использованием функций Function(<список_параметров>:<тип_параметров>)<тип_возвращаемого_результата>; Формальные параметры рекурсивной функции представляют обобщенные начальные данные для текущего рекурсивного вызова подпрограммы Var <список_локальных_переменных>:<тип_переменных>; локальными переменными функции должны быть объявлены все переменные, которые имеют отношение к протеканию текущего шага процесса. начало If n=<значение_1> Условие выхода из рекурсивного алгоритма, т.е. условие при котором очередной рекурсивный вызов уже не производится. При этом должны быть определено значение функции то<значение_функции_1> иначе If то <значение_функции_2> иначе Фактические параметры рекурсивного вызова представляют начальное состояние для следующего рекурсивного вызова if то F:= F(<список_параметров_1>) иначе F:= F(<список_параметров_2>); {следующий рекурсивный вызов с измененными параметрами подпрограммы} конец; Начало {Инициализация начальных значений списка фактических параметров рекурсивной подпрограммы} <имя_переменной>:=F(<список_фактических_параметров>); Конец. При использовании рекурсивных подпрограмм важен вопрос о выделении памяти под локальные переменные: когда разворачивается рекурсивный вызов, то на каждый конкретный вход в подпрограмму будет выделяться память под все локальные переменные. Это значит, что их использование должно быть сведено к минимуму, иначе память может закончиться раньше, чем произойдет вычисление результатирующего значения функции. Рекурсивные и итерационные алгоритмы Математически доказано, что любой итерационный алгоритм может быть заменен рекурсивным (операции сложения, умножения, возведения в степень, функции факториала, предшествования, логарифма, показательная функция, квадратный корень, а также полиномы с положительными коэффициентами). А в теоретическом программировании существует теорема, которая гласит, что рекурсия и итерация эквивалентны. Это значит, что любой алгоритм, который можно реализовать рекурсивно, с таким же успехом может быть реализован и итеративно, и наоборот. В случае явной рекурсии это требование выполняется, поскольку к моменту рекурсивного вызова функции ее заголовок уже определен. При разработке итерационного алгоритма для косвенной рекурсии могут возникнуть некоторые затруднения. Рекурсивные решения менее эффективны относительно времени. Это объясняется необходимостью обрабатывать большое количество вызовов подпрограмм. Тем не менее, бывают случаи, когда переход к рекурсии от хорошего итеративного алгоритма будет давать преимущества, несмотря на то, что реализованы одинаково. Это алгоритмы, в которых используется стек, так как стек и рекурсия взаимозаменяемы. То есть, все что можно сделать при помощи рекурсии, можно заменить на "условно бесконечный" цикл с использованием стека и наоборот. Это очень часто используется тогда, когда размер системного стека (того, в который помещается адрес возврата и где выделяется память под локальные переменные) сильно ограничен какими-то аппаратными особенностями; в таких случаях стек реализуют самостоятельно. Тем не менее, аппаратный стек несколько быстрее, чем стек, реализованный самостоятельно. Поэтому в некоторых случаях, когда используется стек небольших размеров, имеет смысл использовать рекурсию для того, что бы воспользоваться аппаратными возможностями по его организации. Например, для вычисления значения выражения, для перевода выражения из инфиксной в постфиксную форму и т.п. Во многих случаях рекурсия предоставляет возможность использовать естественное, простое решение задачи, которую иными способами решать было бы трудно. Рекурсия – это важное и мощное средство для решения задач и программирования. Достоинства и недостатки рекурсивных подпрограмм Если сравнить рекурсивный и итерационный алгоритм вычисления факториала, то очевидно, насколько рекурсивное решение неэкономно использует оперативную память (и чем больше аргумент функции, тем сильнее разница). Да и по быстродействию оно оказывается слабее. Единственно, чем оно лучше, так это простотой текста функции: мы заставили систему программирования саму организовать циклическое выполнение операций, вместо того, чтобы программировать цикл. Рекурсивные функции дают материал для размышлений о процессе программирования и об его связи с другими областями деятельности. В частности, принцип программирования рекурсивных функций имеет много общего с методом математической индукции. Этот метод используется для доказательства корректности утверждений для бесконечной последовательности состояний, а именно: если утверждение верно в начальном состоянии, а из его справедливости в n-м состоянии можно доказать его справедливость в n+1-м состоянии, то такое утверждение будет справедливым всегда. Этот принцип неявно и применялся нами при разработке рекурсивных функций. Действительно, сама рекурсивная функция представляет собой переход из n-го в n+1 состояние некоторого процесса. Если этот переход корректен, то есть соблюдение некоторых условий на входе функции приводит к их соблюдению на выходе, (то есть в рекурсивном вызове), то эти условия будут соблюдаться во всей цепочке состояний (при безусловной корректности начального вызова). Аспект использования механизма рекурсии состоит, как правило, в повышении уровня простоты и наглядности описания, что приводит к увеличению показателей надежности. Во многих случаях рекурсия предоставляет возможность использовать естественной, простое решение задачи, которую иными способами решить было бы трудно Демонстрационные примеры Пример вычисления функции Cosin(x) Пример: Вычислить функцию с точностью , используя рекурсивную организацию вычислений и определить количество членов ряда, которые необходимо просуммировать, для получения значения функции с заданной точностью. Идея решения заключается в следующем. Будем считать, что мы получили значение функции с заданной точностью  в том случае, когда абсолютное значение разницы между соседними членами ряда больше  До тех пор пока заданная точность не будет достигнута, следует наращивать сумму членов ряда на значение очередного члена ряда Y(n): Значение очередного члена ряда будем определять через значение предыдущего члена ряда по формуле: Затем должен следовать рекурсивный вызов процедуры. Что означает Имя в программе Тип Аргумент функции x float Точность вычислений e float Предыдущее слагаемое Y0 float Текущее слагаемое Y1 float Текущий номер i int Количество членов ряда n int Значение синуса symma float Алгоритм решения задачи Программа AbsX; Аргументы x, e,y1,yn,summ:float; n int; Функция RX(float y1, float yn, float float x, float e, int* n , float *summa); начало Если abs(abs(yn)-abs(y1))>=e то начало n:=n+1; y1:=yn; yn:=(-1)*y1*sqr(x)/((2*n-3)*(2*n-2)); summa:=summa+y1; RX(y1,yn,x,e,n,summa); все конец; начало Ввод(x); Ввод(e); y1:=1; yn:=-sqr(x)/2; n:=2; summ:=y1+yn; RX(y1,yn,x,e,n,summa); конец; Вычислить сумму элементов линейного массива A[1..N]. Int function Summa Вход N : Byte, A: LinMas; Начало Если N = 0 то Summa := 0 иначе Summa := A[N] + Summa(N - 1, A) Конец; Cумма элементов массива A[1..N] равна нулю, если количество элементов массива равно нулю, и сумме всех предыдущих элементов плюс последний, если количество элементов не равно нулю. Определение количества цифр во вводимом с клавиатуры числе char K(N:Longint) Начало Если N<10 То K:=1 Иначе K:=K(N div 10)+1 Конец Рекуррентное соотношение определения функции K(n), которая возвращает количество цифр в заданном натуральном числе n: Возведение в степень int power (a,n: int) { Если (n == 0) power= 1; иначе { Если (n%2 == 0) power= power(a*2, n div 2); иначе power= power(a, n-1)*a; } {ИЛИ t= power(a, n div 2); power:= t*t;} конец; Алгоритм двоичного поиска. Рекурсия и поисковые задачи С помощью рекурсии легко решаются задачи, связанные с поиском, основанном на полном или частичном переборе возможных вариантов. Принцип рекурсивности заключается здесь в том, что процесс поиска разбивается на шаги, на каждом из которых выбирается и проверяется очередной элемент из множества, а алгоритм поиска повторяется, но уже для оставшихся данных. При реализации поисковых задач следует обратить внимание на результат рекурсивной функции. Он непосредственно связан с организацией процесса поиска и перебора вариантов: Если рекурсивная функция выполняет поиск первого элемента , удовлетворяющего требованию, результатом ее является как правило логическое значение: истина соответствует успешному завершению поиска, а ложь - неудачному. Общим для всех алгоритмов поиска является: если рекурсивный вызов возвращает истинное значение, то рекурсивные вызовы должны прекратится. Если рекурсивный вызов возвращает ложь, по поиск должен быть продолжен. Если в процессе поиска производится более сложный анализ и сравнение вариантов, то рекурсивная подпрограмма и, соответственно, шаг процесса должны производить выбор между подходящими вариантами, с целью выбора наиболее оптимального. Обычно для этого используется минимум или максимум какой-либо характеристики выбираемого варианта. Тогда рекурсивная подпрограмма возвращает значение, которое является оценкой для оставшихся не просмотренных элементов, а текущий рекурсивный вызов выбирает из них минимум или максимум с учетом данных текущего шага. Алгоритм Двоичный поиск Вход l,r int {левая и правая границы массива, в котором осуществляется поиск} Выход m: порядковый номер найденного элемента или значение истина; Начало Если левая граница <= правой, то Начало Найти середину области поиска m:=(l+r)/2; Если искомое число <а[m], то Двоичный поиск (l,m-1); Если искомое число >а[m], то Двоичный поиск (m+1,r) Если искомое число =а[m], то искомое число найдено. Конец Иначе Сообщить, что искомого числа в массиве нет. Нерекурсивный алгоритм двоичного поиска. Начало Установить левую границу поиска =1; Установить правую границу поиска =N; Пока левая граница <= правой Нц Найти середину области поиска m:=(l+r)/2; Если искомое число <а[m], то изменить правую границу R:=m-1; Если искомое число >а[m], то изменить левую границу L:=m+1; Если искомое число =а[m], то искомое число найдено. Кц Сообщить, что искомого числа в массиве нет. 19 Массивы: описание, внутреннее представление. Примеры работы с одномерными массивами: инициализация, ввод/вывод, суммирование значений, поиск элемента, слияние массивов, разбиение массивов, сдвиг элементов в массиве, удаление и вставка элементов Массивы и строки Массив — это набор переменных одного типа, имеющих одно и то же имя. Доступ к конкретному элементу массива осуществляется с помощью индекса. В языке С все массивы располагаются в отдельной непрерывной области памяти. Первый элемент массива располагается по самому меньшему адресу, а последний — по самому большому. Массивы могут быть одномерными и многомерными. Строка представляет собой массив символьных переменных, заканчивающийся специальным нулевым символом, это наиболее распространенный тип массива. Массивы и указатели тесно связаны. То, что может быть сказано о массивах, чаще всего непосредственно относится и к указателям, и наоборот. Одномерные массивы Общая форма объявления одномерного массива имеет следующий вид: тип имя_переменной [размер]; Как и другие переменные, массив должен быть объявлен явно, чтобы компилятор выделил для него определенную область памяти (т.е. разместил массив). Здесь тип обозначает базовый тип массива, являющийся типом каждого элемента. Размер задает количество элементов массива. Например, следующий оператор объявляет массив из 100 элементов типа double под именем balance: double balance[100]; Согласно стандарту С89 размер массива должен быть указан явно с помощью выражения-константы. Таким образом, в программе на С89 размер массива определяется во время компиляции и впоследствии остается неизменным. (В С99 определены массивы, размер которых определяется во время выполнения). Доступ к элементу массива осуществляется с помощью имени массива и индекса. Индекс элемента массива помещается в квадратных скобках после имени. Например, оператор balance[3] = 12.23; присваивает 3-му элементу массива balance значение 12.23. Индекс первого элемента любого массива в языке С равен нулю. Поэтому оператор char p[10]; объявляет массив символов из 10 элементов — от р[0] до р[9]. В следующей программе вычисляются значения элементов массива целого типа с индексами от 0 до 99: #include int main(void) { int x[100]; /* объявление массива 100 целых */ int t; /* присваение массиву значений от 0 до 99 */ for(t=0; t<100; ++t) x[t] = t; /* вывод на экран содержимого x */ for(t=0; t<100; ++t) printf("%d ", x[t]); return 0; } Объем памяти, необходимый для хранения массива, непосредственно определяется его типом и размером. Для одномерного массива количество байтов памяти вычисляется следующим образом: количество_байтов = sizeof(базовый_тип) × длина_массива Во время выполнения программы на С не проверяется ни соблюдение границ массивов, ни их содержимое. В область памяти, занятую массивом, может быть записано что угодно, даже программный код. Программист должен сам, где это необходимо, ввести проверку границ индексов. Следующий пример программы компилируется без ошибки, однако при выполнении происходит нарушение границы массива count и разрушение соседних участков памяти: int count[10], i; /* здесь нарушена граница массива count */ for(i=0; i<100; i++) count[i] = i; Можно сказать, что одномерный массив — это список, хранящийся в непрерывной области памяти в порядке индексации. В памяти массив а, начинающийся по адресу 1000 и объявленный как char a[7]; хранится следующим образом: Массив из семи символов, начинающийся по адресу 1000 Элемент a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] Адрес 1000 1001 Создание указателя на массив Указатель на 1-й элемент массива можно создать путем присваивания ему имени массива без индекса. Например, если есть объявление int sample[10]; то в качестве указателя на 1-й элемент массива можно использовать имя sample. В следующем фрагменте программы адрес 1-го элемента массива sample присваивается указателю р: int *p; int sample[10]; p = sample; В обеих переменных (р и sample) хранится адрес 1-го элемента, отличаются эти переменные только тем, что значение sample в программе изменять нельзя. Адрес первого элемента можно также получить, используя оператор получения адреса &. Например, выражения sample и &sample[0] имеют одно и то же значение. Тем не менее, в профессионально написанных программах вы не встретите выражения &sample[0]. Передача одномерного массива в функцию В языке С нельзя передать весь массив как аргумент функции. Однако можно передать указатель на массив, т.е. имя массива без индекса. Например, в представленной программе в func1() передается указатель на массив i: int main(void) { int i[10]; func1(i); /* ... */ } Если в функцию передается указатель на одномерный массив, то в самой функции его можно объявить одним из трех вариантов: как указатель, как массив определенного размера и как массив без определенного размера. Например, чтобы функция func1() получила доступ к значениям, хранящимся в массиве i, она может быть объявлена как void func1(int *x) /* указатель */ { /* ... */ } или как void func1(int x[10]) /* массив определенного размера */ { /* ... */ } и наконец как void func1(int x[]) /* массив без определенного размера */ { /* ... */ } Эти три объявления тождественны, потому что каждое из них сообщает компилятору одно и то же: в функцию будет передан указатель на переменную целого типа. В первом объявлении используется указатель, во втором — стандартное объявление массива. В последнем примере измененная форма объявления массива сообщает компилятору, что в функцию будет передан массив неопределенной длины. Как видно, длина массива не имеет для функции никакого значения, потому что в С проверка границ массива не выполняется. Эту функцию можно объявить даже так: void func1(int x[32]) { /* ... */ } И при этом программа будет выполнена правильно, потому что компилятор не создает массив из 32 элементов, а только подготавливает функцию к приему указателя. Индексация указателей Указатели и массивы тесно связаны друг с другом. Имя массива без индекса — это указатель на первый (начальный) элемент массива. Рассмотрим, например, следующий массив: char p[10]; Следующие два выражения идентичны: p &p[0] Выражение p == &p[0] принимает значение ИСТИНА, потому что адрес 1-го элемента массива — это то же самое, что и адрес массива. Имя массива без индекса представляет собой указатель. И наоборот, указатель можно индексировать как массив. Рассмотрим следующий фрагмент программы: int *p, i[10]; p = i; p[5] = 100; /* в присваении используется индекс */ *(p+5) = 100; /* в присвоении используется адресная арифметика */ Оба оператора присваивания заносят число 100 в 6-й элемент массива i. Первый из них индексирует указатель p, во втором применяются правила адресной арифметики. В обоих случаях получается один и тот же результат. Можно также индексировать указатели на многомерные массивы. Например, если а — это указатель на двухмерный массив целых размерностью 10×10, то следующие два выражения эквивалентны: a &a[0][0] Более того, к элементу (0,4)[1] можно обратиться двумя способами: либо указав индексы массива: а[0][4], либо с помощью указателя: *((int*)а+4). Аналогично для элемента (1,2): а[1][2] или *((int*)а+12). В общем виде для двухмерного массива справедлива следующая формула: a[j][k] эквивалентно *((базовый_тип*)а+(j*длина_строки)+k) Правила адресной арифметики требуют явного преобразования указателя на массив в указатель на базовый тип. Указатели используются для обращения к элементам массива потому, что часто операции адресной арифметики выполняются быстрее, чем индексация массива. Двухмерный массив может быть представлен как указатель на массив одномерных массивов. Добавив еще один указатель, можно с его помощью обращаться к элементам отдельной строки массива. Этот прием демонстрируется в функции pr_row(), которая печатает содержимое конкретной строки двухмерного глобального массива num: int num[10][10]; /* ... */ void pr_row(int j) { int *p, t; p = (int *) &num[j][0]; /* вычисление адреса 1-го элемента строки номер j */ for(t=0; t<10; ++t) printf("%d ", *(p+t)); } Эту функцию можно обобщить, включив в список аргументов номер строки, длину строки и указатель на 1-й элемент: void pr_row(int j, int row_dimension, int *p) { int t; p = p + (j * row_dimension); for(t=0; t А [2] > ... > A[N]; • по невозрастанию - каждый следующий элемент не больше предыдущего А[1]>= А[2] >=...>=A[N]. Наиболее часто используются следующие алгоритмы: метод пузырька, метод прямого выбора, метод простых вставок (включений), метод Шелла, метод Хоара. Большинство остальных алгоритмов являются различными модификациями вышеперечисленных алгоритмов. Прямые методы особенно удобны для объяснения основных принципов большинства сортировок. Они также являются хорошим примером того, что путем усложнения алгоритма (хотя существуют очевидные методы) можно добиться значительного выигрыша в эффективности. Методы сортировки "на том же месте" можно разбить на три основные категории: • сортировки с помощью включения; • сортировки с помощью выделения; • сортировки с помощью обменов. Метод простых вставок Метод сортировки, который часто применяют игроки в карты по отношению к картам на руках, заключается в том, что отдельно анализируется каждый конкретный элемент, который затем помещается в надлежащее место среди других, уже отсортированных элементов. В условиях компьютерной реализации необходимо позаботиться о том, чтобы освободить место для вставляемого элемента путем смещения больших элементов на одну позицию вправо, после чего на освободившееся место помещается вставляемый элемент. Алгоритм: элементы массива мысленно делятся на уже "готовую" и исходную последовательности. При каждом шаге, начиная с i=2 и увеличивая i каждый раз на единицу, из исходной последовательности извлекается i-й элемент и перекладывается в готовую последовательность, при этом он вставляется на нужное место. В процессе поиска подходящего места для элемента х удобно, чередуя сравнения и движения по последовательности, как бы просеивать х, т.е. х сравнивается с очередным элементом aj, а затем либо х вставляется на свободное место, либо aj сдвигается вправо и процесс "уходит" влево. Процесс просеивания может закончиться при выполнении одного из двух условий: - найден элемент aj с ключом, меньшим чем ключ у х; - достигнут левый конец готовой последовательности. Повторяющийся процесс с двумя условиями окончания позволяет воспользоваться приемом "барьера", поставив барьер a0 со значением х. Итак, алгоритм сортировки: -Сначала находим первый элемент массива, неупорядоченный по возрастанию -Найти место вставки -Сдвинуть часть данных a0 - ячейка-барьер. Используется для того, чтобы остановить поиск Имеется числовой массив а[0..N] i-порядковый n элемента, который будет вставляться, будет меняться от 2 до n j- граница упорядоченной части, будет меняться от (i-1) до 0 с шагом (-1)} Реализация алгоритма Для I от 2 до N Нц запоминаем вставляемый элемент вставляем его в барьер. Барьер - лишняя ячейка памяти, которая позволяет ускорить сортировку Пока xx. В нашем случае в правой части все элементы тяжелее Х, Þ, кандидат для обмена само Х. Если элемент найден (он может быть =х), то меняем их местами. Поэтому меняем местами a[1] и a[4]. При этом i=i+1, j=j+1. Имеем , I:= 1 2 3 4 5 6 7 A[I] 3 11 1 5 4 12 7 Þ I = I + 1=2; j = j – 1=3. Меняем местами a[2] и a[3]. Имеем ,. I:= 1 2 3 4 5 6 7 A[I] 3 1 11 5 4 12 7 Þ I = I + 1=3; j = j – 1=2. Таким образом повторяем выполнение п. 1-4 до того, как i>j, что является признаком упорядоченности. Имеем , Þ I = I + 1; j = j – 1. Легкая Тяжелая часть I:= 1 2 3 4 5 6 7 A[I] 3 11 1 5 4 12 7 Теперь массив разделен на легкую и тяжелую части, но эти части неупорядочены. Следовательно нужно повторить алгоритм для этих частей поочередно до тех пор, пока в каждой из частей массива не останется по одному элементу, то есть пока не будет отсортирован весь массив.. Конечность глубины рекурсии гарантируется тем, что длина сортируемого участка на каждом уровне рекурсии уменьшается хотя бы на 1 . РЕАЛИЗАЦИЯ АЛГОРИТМА: (*------Алгоритм быстрой сортировки массива---*) Алгоритм SORT Вход b : mass; выход a : mass); Тип index=0..n; Алгоритм QuickSort( l, r :I ndex); Переменные i, j: index; x, w: float; Начало i := l; j := r; x := a[(i+j) DIV 2]; Повторять Нц Пока a[i]j; Если lx. В нашем случае в правой части все элементы тяжелее Х, Þ, кандидат для обмена само Х. Если элемент найден (он может быть =х), то меняем их местами. Поэтому меняем местами a[1] и a[4]. При этом i=i+1, j=j+1. Имеем , I:= 1 2 3 4 5 6 7 A[I] 3 11 1 5 4 12 7 Þ I = I + 1=2; j = j – 1=3. Меняем местами a[2] и a[3]. Имеем ,. I:= 1 2 3 4 5 6 7 A[I] 3 1 11 5 4 12 7 Þ I = I + 1=3; j = j – 1=2. Таким образом повторяем выполнение п. 1-4 до того, как i>j, что является признаком упорядоченности. Имеем , Þ I = I + 1; j = j – 1. Легкая Тяжелая часть I:= 1 2 3 4 5 6 7 A[I] 3 11 1 5 4 12 7 Теперь массив разделен на легкую и тяжелую части, но эти части неупорядочены. Следовательно, алгоритм должен повторяться. Причем части должны упорядочиваться поочередно. Стоит вопрос, какую из частей сортировать в первую очередь. У Вирта на этот счет существует несколько идей. Мы используем следующую: Помещаем границы массивов в стеки. L – в левый, R – в правый. L_St, R_St –границы необработанной части массивов. L_St R_St Исходный массив 3 7 L R I:= 1 2 3 4 5 6 7 A[I] 3 11 1 5 4 12 7 Легкая часть j i Сортируем легкую часть, R:=J L:=1 R:=2 3 1 I:=1 J:=R Медианный элемент (1+2) div 2=1; 1 3 J ® ¬i I=R; {1} До s=0; Начало s:=1; L_St[s]:=1; R_st[s]:=n; {1} Повторять {1 - Внешний цикл} Нц L:=L_St[s]; R:=R_st[s]; s:=s-1; {2} Повторять {2 - Внутренний цикл} Нц i:=L; j:=R; x:=a[((i+j) div 2)]; {3} Повторять {3- Внутренний цикл} Нц Пока a[i]x делать Нц j:=j-1; Кц Если i<=j то начало y:=a[i]; a[i]:=a[j]; a[j]:=y; i:=i+1; j:=j-1; все {если} {3} Кц До i>j; Если i=R Кц {1}До S=0; Конец; Метод Шелла Метод Шелла предложен автором Donald Lewis Shеll в 1959 г. Основная идея этого алгоритма заключается в том, чтобы в начале устранить массовый беспорядок в массиве, сравнивая далеко стоящие друг от друга элементы. Как видно, интервал между сравниваемыми элементами (gap) постепенно уменьшается до единицы. Это означает, что на поздних стадиях сортировка сводится просто к перестановкам соседних элементов (если, конечно, такие перестановки являются необходимыми). Сортировка методом Шелла это усовершенствованный метод прямого включения. Сначала отдельно группируются и сортируются элементы, отстоящие друг от друга на расстоянии 4.Такой процесс называется четвертной сортировкой. После первого прохода элементы перегруппировываются - теперь каждый элемент группы отстоит от другого на 2 позиции - и вновь сортируются. Это называется двойной сортировкой. И наконец, на третьем проходе идет обычная или одинарная сортировка Например: Дан массив, состоящий из элементов: 44 55 12 42 94 18 06 67 После четверной сортировки получаем: 44 18 06 42 94 55 12 67 Т.е. элементы, отстоящие на 4 позиции друг от друга сравниваются и меняются местами в случае необходимости (44<94, 55>18 значит меняем местами, 12>06 тоже меняем местами, 42<67). После двойной сортировки получаем: 06 18 12 42 44 55 94 67 Теперь сравниваем элементы, отстоящие на 2 позиции (44>06 - меняем, 94>12 - меняем, 44>12 - меняем и т.д.) После одинарной сортировки получаем: 06 12 18 42 44 55 67 94 На первый взгляд можно засомневаться: если необходимо несколько процессов сортировки, причем в каждый включаются все элементы, то не добавят ли они больше работы, чем сэкономят? Однако на каждом этапе либо сортируется относительно мало элементов, либо элементы уже довольно хорошо упорядочены и требуется сравнительно немного перестановок. Ясно, что такой метод в результате дает упорядоченный массив, и, конечно же, сразу видно, что каждый проход от предыдущих только выигрывает (так как каждая i-сортировка объединяет две группы, уже отсортированные 2i-сортировкой). Так же очевидно, что расстояния в группах можно уменьшать по-разному, лишь бы последнее было единичным, ведь в самом плохом случае последний проход и сделает всю работу. Все t расстояния обозначаются соответственно h1,h2,...,ht, для них выполняются условия ht=1, hi+11 код разделяет массив на два, размер каждого из которых меньше N , исходя из индуктивного предположения, находит макс. эл-ты в обеих частях и возвращает большее из этих двух значений, которое должно быть максимальным значением для всего массива. Лемма Рекурсивная функция, которая разделяет задачу размерности N на две независимые (непустые) решающие ее части, рекурсивно вызывает себя менее N раз. Лемма. Бинарный поиск исследует не более lgN +1 чисел. Бинарный поиск позволяет решить большую задачу с 1 миллионом чисел при помощи 20 сравнений. Нерекурсивный алгоритм двоичного поиска. Начало Установить левую границу поиска =1; Установить правую границу поиска =N; Пока левая граница <= правой Нц Найти середину области поиска m:=(l+r)/2; Если искомое число <а[m], то изменить правую границу R:=m-1; Если искомое число >а[m], то изменить левую границу L:=m+1; Если искомое число =а[m], то искомое число найдено. Кц Сообщить, что искомого числа в массиве нет. Рекурсивный алгоритм двоичного поиска. Алгоритм Двоичный поиск (l,r) Начало Если левая граница <= правой, то Нц Найти середину области поиска m:=(l+r)/2; Если искомое число <а[m], то Двоичный поиск (l,m-1); Если искомое число >а[m], то Двоичный поиск (m+1,r) Если искомое число =а[m], то искомое число найдено. Кц Иначе Сообщить, что искомого числа в массиве нет. Применение алгоритма «разделяй и властвуй» для отыскания максимального из N элементов массива а[1..n]. Не рекурсивный алгоритм решает задачу за один проход. Используемые обозначения:L– левая граница, R-правая граница, M-средний элемент. Рекурсивный алгоритм нахождения макс. элемента. Алгоритм Макс (а,l,r) Начало Если левая граница <= правой, то Нц Найти середину области поиска m:=(l+r)/2; U:= Макс (а,l,m); V:= Макс (а,m+1,r) Если u>v то Макс:= u, иначе Макс:= v Кц Многие алгоритмы типа «разделяй и властвуй» имеют одинаковую рекурсивную структуру: CN = 2*CN/2 + N, где N>=2 и C1 =0. Некоторые алгоритмы типа «разделяй и властвуй» могут выполнять различный объем вычислений для различных вызовов функций, время выполнения таких алгоритмов зависит от конкретного способа разделения на части. В случае, когда задача делится пополам, а затем алгоритм работает только с одной половиной (сумма размеров частей равна общей размерности) – время выполнения линейно связано с количеством вызовов. Другие алгоритмы типа «разделяй и властвуй» могут разделять задачу на части, сумма размеров которых меньше или больше размерности всей задачи. Эти алгоритмы все же относятся к алгоритмам типа «разделяй и властвуй», поскольку каждая часть меньше целого, но анализировать их труднее. Бинарный поиск элемента, равного у в упорядоченном массиве Оператор цикла «Пока» L:=1; R:=n; Found:=false; Result:=-1; While (l<=r) и not found do начало Mid:=round((l+r)/2); Если a[mid]=y то начало result:=mid; found:=true конец иначе Если yr) or found; Если found то Write(' Search digit found in massiv at number ',result) иначе Write(' Search digit not found in massiv '); Рекурсивный алгоритм Алгоритм BS Входl,r int; Выход a:tarray; number int; flag:char; Локальные переменные m int; начало Если l<=r то начало m:=round((r+l)/ 2) ; Если x=a[m] то начало number:=m; flag:=true; конец; Если x int main(void) { int t, i, num[3][4]; for(t=0; t<3; ++t) for(i=0; i<4; ++i) num[t][i] = (t*4)+i+1; /* вывод на экран */ for(t=0; t<3; ++t) { for(i=0; i<4; ++i) printf("%3d ", num[t][i]); printf("\n"); } return 0; } В этом примере num[0][0] имеет значение 1, num[0][1] — значение 2, num[0][2] — значение 3 и так далее. Наглядно двухмерный массив num можно представить так: num[t][i] | 0 1 2 3 --+----------- 0 | 1 2 3 4 2 | 5 6 7 8 3 | 9 10 11 12 Двухмерные массивы размещаются в матрице, состоящей из строк и столбцов. Первый индекс указывает номер строки, а второй — номер столбца. Это значит, что когда к элементам массива обращаются в том порядке, в котором они размещены в памяти, правый индекс изменяется быстрее, чем левый. Объем памяти в байтах, занимаемый двухмерным массивом, вычисляется по следующей формуле: количество_байтов = = размер_1-го_измерения × размер_2-го_измерения × sizeof(базовый_тип) Например, двухмерный массив 4-байтовых целых чисел размерностью 10×5 занимает участок памяти объемом 10×5×4 то есть 200 байтов. Если двухмерный массив используется в качестве аргумента функции, то в нее передается только указатель на начальный элемент массива. В соответствующем параметре функции, получающем двухмерный массив, обязательно должен быть указан размер правого измерения[1], который равен длине строки массива. Размер левого измерения указывать не обязательно. Размер правого измерения необходим компилятору для того, чтобы внутри функции правильно вычислить адрес элемента массива, так как для этого компилятор должен знать длину строки массива. Например, функция, получающая двухмерный массив целых размерностью 10×10, должна быть объявлена так: void func1(int x[][10]) { /* ... */ } Компилятор должен знать длину строки массива, чтобы внутри функции правильно вычислить адрес элемента массива. Если при компиляции функции это неизвестно, то невозможно определить, где начинается следующая строка, и вычислить, например, адрес элемента x[2][4] В следующем примере двухмерные массивы используются для хранения оценок студентов. Предполагается, что преподаватель ведет три класса, в каждом из которых учится не более 30 студентов. Обратите внимание на то, как происходит обращение к массиву grade в каждой функции. /* Простая база данных оценок студентов. */ #include #include #include #define CLASSES 3 #define GRADES 30 int grade[CLASSES][GRADES]; void enter_grades(void); int get_grade(int num); void disp_grades(int g[][GRADES]); int main(void) { char ch, str[80]; for(;;) { do { printf("(В)вод оценок студентов\n"); printf("В(ы)вод оценок студентов\n"); printf("Вы(х)од\n"); gets(str); ch = toupper(*str); } while(ch!='В' && ch!='ы' && ch!='х'); switch(ch) { case 'В': enter_grades(); break; case 'ы': disp_grades(grade); break; case 'х': exit(0); } } return 0; } /* Занесение оценок студентов в массив. */ void enter_grades(void) { int t, i; for(t=0; t #define MAX 100 #define LEN 80 char text[MAX][LEN]; int main(void) { register int t, i, j; printf("Для выхода введите пустую строку.\n"); for(t=0; t #include char matrix[3][3]; /* матрица игры */ char check(void); void init_matrix(void); void get_player_move(void); void get_computer_move(void); void disp_matrix(void); int main(void) { char done; printf("Это игра в крестики-нолики.\n"); printf("Вы будете играть против компьютера.\n"); done = ' '; init_matrix(); do { disp_matrix(); get_player_move(); done = check(); /* проверка, есть ли победитель */ if(done!= ' ') break; /* есть победитель */ get_computer_move(); done = check(); /* проверка, есть ли победитель */ } while(done== ' '); if(done=='X') printf("Вы победили!\n"); else printf("Победил компьютер!!!!\n"); disp_matrix(); /* показ финальной позиции */ return 0; } /* Инициализация матрицы игры. */ void init_matrix(void) { int i, j; for(i=0; i<3; i++) for(j=0; j<3; j++) matrix[i][j] = ' '; } /* Ход игрока. */ void get_player_move(void) { int x, y; printf("Введите координаты X,Y Вашего хода: "); scanf("%d%*c%d", &x, &y); x--; y--; if(matrix[x][y]!= ' '){ printf("Неверный ход, попытайтесь еще.\n"); get_player_move(); } else matrix[x][y] = 'X'; } /* Ход компьютера. */ void get_computer_move(void) { int i, j; for(i=0; i<3; i++){ for(j=0; j<3; j++) if(matrix[i][j]==' ') break; if(matrix[i][j]==' ') break; /* Второй break нужен для выхода из цикла по i */ } if(i*j==9) { printf("Конец игры\n"); exit(0); } else matrix[i][j] = 'O'; } /* Вывод матрицы на экран. */ void disp_matrix(void) { int t; for(t=0; t<3; t++) { printf(" %c | %c | %c ",matrix[t][0], matrix[t][1], matrix [t][2]); if(t!=2) printf("\n---|---|---\n"); } printf("\n"); } /* Определение победителя. */ char check(void) { int i; for(i=0; i<3; i++) /* проверка строк */ if(matrix[i][0]==matrix[i][1] && matrix[i][0]==matrix[i][2]) return matrix[i][0]; for(i=0; i<3; i++) /* проверка столбцов */ if(matrix[0][i]==matrix[1][i] && matrix[0][i]==matrix[2][i]) return matrix[0][i]; /* проверка диагоналей */ if(matrix[0][0]==matrix[1][1] && matrix[1][1]==matrix[2][2]) return matrix[0][0]; if(matrix[0][2]==matrix[1][1] && matrix[1][1]==matrix[2][0]) return matrix[0][2]; return ' '; } Пояснение к программе. В функции get_player_move() с помощью библиотечной функции scanf() считываются с клавиатуры два целых числа x и y. Функция scanf() при считывании чисел предполагает, что во входном потоке они разделены пробелами (или пробельными символами), другие разделительные символы не допускаются. Однако многие пользователи привыкли к тому, что числа можно разделять, например, запятыми. (Собственно говоря, именно так и предлагается в подсказке, выдаваемой программой.) В приведенном примере символ, следующий непосредственно после первого числа, просто игнорируется, именно для этого в функции scanf() используется спецификатор формата %*c. Звездочка означает, что символ считывается из потока, но в память не записывается. Многомерные массивы В языке С можно пользоваться массивами, размерность которых больше двух. Общая форма объявления многомерного массива следующая: тип имя_массива [Размер1][Размер2]...[РазмерN]; Массивы, у которых число измерений больше трех, используются довольно редко, потому что они занимают большой объем памяти. Например, четырехмерный массив символов размерностью 10x6x9x4 занимает 2160 байтов. Если бы массив содержал 2-байтовые целые, потребовалось бы 4320 байтов. Если бы элементы массива имели тип double, причем каждый элемент (вещественное число двойной точности) занимал бы 8 байтов, то для хранения массива потребовалось бы 17280 байтов. Объем требуемой памяти с ростом числа измерений растет экспоненциально. Например, если к предыдущему массиву добавить пятое измерение, причем его толщину по этому измерению сделать равной всего 10, то его объем возрастет до 172800 байтов. При обращении к многомерным массивам компьютер много времени затрачивает на вычисление адреса, так как при этом приходится учитывать значение каждого индекса. Поэтому доступ к элементам многомерного массива происходит значительно медленнее, чем к элементам одномерного. Передавая многомерный массив в функцию, в объявлении параметров функции необходимо указать все размеры измерений, кроме самого левого. Например, если массив m объявлен как int m[4] [3] [6] [5]; то функция, принимающая этот массив, должна быть объявлена примерно так: void func1(int d[][3][6][5]) { /* ... */ } Конечно, можно включить в объявление и размер 1-го измерения, но это излишне. Индексация указателей Указатели и массивы тесно связаны друг с другом. Имя массива без индекса — это указатель на первый (начальный) элемент массива. Рассмотрим, например, следующий массив: char p[10]; Следующие два выражения идентичны: p &p[0] Выражение p == &p[0] принимает значение ИСТИНА, потому что адрес 1-го элемента массива — это то же самое, что и адрес массива. Имя массива без индекса представляет собой указатель. И наоборот, указатель можно индексировать как массив. Рассмотрим следующий фрагмент программы: int *p, i[10]; p = i; p[5] = 100; /* в присваении используется индекс */ *(p+5) = 100; /* в присвоении используется адресная арифметика */ Оба оператора присваивания заносят число 100 в 6-й элемент массива i. Первый из них индексирует указатель p, во втором применяются правила адресной арифметики. В обоих случаях получается один и тот же результат Можно также индексировать указатели на многомерные массивы. Например, если а — это указатель на двухмерный массив целых размерностью 10×10, то следующие два выражения эквивалентны: a &a[0][0] Более того, к элементу (0,4)[1] можно обратиться двумя способами: либо указав индексы массива: а[0][4], либо с помощью указателя: *((int*)а+4). Аналогично для элемента (1,2): а[1][2] или *((int*)а+12). В общем виде для двухмерного массива справедлива следующая формула: a[j][k] эквивалентно *((базовый_тип*)а+(j*длина_строки)+k) Правила адресной арифметики требуют явного преобразования указателя на массив в указатель на базовый тип. Указатели используются для обращения к элементам массива потому, что часто операции адресной арифметики выполняются быстрее, чем индексация массива. Двухмерный массив может быть представлен как указатель на массив одномерных массивов. Добавив еще один указатель, можно с его помощью обращаться к элементам отдельной строки массива. Этот прием демонстрируется в функции pr_row(), которая печатает содержимое конкретной строки двухмерного глобального массива num: int num[10][10]; /* ... */ void pr_row(int j) { int *p, t; p = (int *) &num[j][0]; /* вычисление адреса 1-го элемента строки номер j */ for(t=0; t<10; ++t) printf("%d ", *(p+t)); } Эту функцию можно обобщить, включив в список аргументов номер строки, длину строки и указатель на 1-й элемент: void pr_row(int j, int row_dimension, int *p) { int t; p = p + (j * row_dimension); for(t=0; t int main(void) { int x = 99; int *p1, *p2; p1 = &x; p2 = p1; /* печать значение x дважды */ printf("Значение по адресу p1 и p2: %d %d\n", *p1, *p2); /* печать адреса x дважды */ printf("Значение указателей p1 и p2: %p %p", p1, p2); return 0; } после присваивания p1 = &x; p2 = p1; оба указателя (p1 и р2) ссылаются на х. То есть, оба указателя ссылаются на один и тот же объект. Программа выводит на экран следующее: Значения по адресу p1 и р2 : 99 99 Значения указателей p1 и р2: 0063FDF0 0063FDF0 Для вывода значений указателей в функции printf() используется спецификатор формата %р, который выводит адреса в формате, используемом компилятором. Допускается присваивание указателя одного типа указателю другого типа. Однако для этого необходимо выполнить явное преобразование типа указателя (операция приведения типов). Преобразование типа указателя Указатель можно преобразовать к другому типу. Эти преобразования бывают двух видов: с использованием указателя типа void * и без его использования. В языке С допускается присваивание указателя типа void * указателю любого другого типа (и наоборот) без явного преобразования типа указателя. Тип указателя void * используется, если тип объекта неизвестен. Например, использование типа void * в качестве параметра функции позволяет передавать в функцию указатель на объект любого типа, при этом сообщение об ошибке не генерируется. Также он полезен для ссылки на произвольный участок памяти, независимо от размещенных там объектов. Например, функция размещения mallocO возвращает значение типа void *, что позволяет использовать ее для размещения в памяти объектов любого типа. В отличие от void *, преобразования всех остальных типов указателей должны быть всегда явными (т.е. должна быть указана операция приведения типов). Преобразование одного типа указателя к другому может вызвать непредсказуемое поведение программы. Например, в следующей программе делается попытка присвоить значение х переменной у посредством указателя р. При компиляции программы сообщение об ошибке не генерируется, однако результат работы программы неверен. #include int main(void) { double x = 100.1, y; int *p; /* В следующем операторе указателю на целое p (присваивается значение, ссылающееся на double. */ p = (int *) &x; /* Следующий оператор работает не так, как ожидается. */ y = *p; /* attempt to assign y the value x through p */ /* Следующий оператор не выведет число 100.1. */ printf("Значение x равно: %f (Это не так!)", y); return 0; } Операция приведения типов применяется в операторе присваивания адреса переменной х (он имеет тип double *) указателю p, тип которого int *. Преобразование типа выполнено корректно, однако программа работает не так, как ожидается (по крайней мере, в большинстве оболочек). Для разъяснения проблемы предположим, что переменная int занимает в памяти 4 байта, а double — 8 байтов. Указатель p объявлен как указатель на целую переменную (т.е. типа int), поэтому оператор присваивания y = *р; передаст переменной y только 4 байта информации, а не 8 байтов, необходимых для double. Несмотря на то, что p ссылается на объект double, оператор присваивания выполнит действие с объектом типа int, потому что p объявлен как указатель на int. Поэтому такое использование указателя p неправильное. Приведенный пример подтверждает то, что операции с указателями выполняются в зависимости от базового типа указателей. Синтаксически допускается ссылка на объект с типом, отличным от типа указателя, однако при этом указатель будет "думать", что он ссылается на объект своего типа. Таким образом, операции с указателями управляются типом указателя, а не типом объекта, на который он ссылается. Разрешен еще один тип преобразований: преобразование целого в указатель и наоборот. В этом случае необходимо применить операцию приведения типов (явное преобразование типа). Однако пользоваться этим средством нужно очень осторожно, потому что при этом легко получить непредсказуемое поведение программы. Явное преобразование типа не обязательно, если преобразуется нуль, то есть нулевой указатель. В языке C++ требуется явно указывать преобразование типа указателей, в том числе указателей типа void *. Поэтому многие программисты используют в языке С явное преобразование для совместимости с C++. Адресная арифметика В языке С допустимы только две арифметические операции над указателями: суммирование и вычитание. Предположим, текущее значение указателя p1 типа int * равно 2000. Предположим также, что переменная типа int занимает в памяти 2 байта. Тогда после операции увеличения p1++; указатель p1 принимает значение 2002, а не 2001. То есть, при увеличении на 1 указатель p1 будет ссылаться на следующее целое число. Это же справедливо и для операции уменьшения. Например, если p1 равно 2000, то после выполнения оператора p1--; значение p1 будет равно 1998. Операции адресной арифметики подчиняются следующим правилам. После выполнения операции увеличения над указателем, данный указатель будет ссылаться на следующий объект своего базового типа. После выполнения операции уменьшения — на предыдущий объект. Применительно к указателям на char, операций адресной арифметики выполняются как обычные арифметические операции, потому что длина объекта char всегда равна 1. Для всех указателей адрес увеличивается или уменьшается на величину, равную размеру объекта того типа, на который они указывают. Поэтому указатель всегда ссылается на объект с типом, тождественным базовому типу указателя. Пример размещения в памяти переменных char (слева) и int (справа) char *ch = (char *) 3000; int *i = (int *) 3000; +------+ ch --->| 3000 |--. +------+ |<- i ch+1 ->| 3001 |--' +------+ ch+2 ->| 3002 |--. +------+ |<- i+1 ch+3 ->| 3003 |--' +------+ ch+4 ->| 3004 |--. +------+ |<- i+2 ch+5 ->| 3005 |--' +------+ Память Операции адресной арифметики не ограничены увеличением (инкрементом) и уменьшением (декрементом). Например, к указателям можно добавлять целые числа или вычитать из них целые числа. Выполнение оператора p1 = p1 + 12; "передвигает" указатель p1 на 12 объектов в сторону увеличения адресов. Кроме суммирования и вычитания указателя и целого, разрешена еще только одна операция адресной арифметики: можно вычитать два указателя. Благодаря этому можно определить количество объектов, расположенных между адресами, на которые указывают данные два указателя; правда, при этом считается, что тип объектов совпадает с базовым типом указателей. Все остальные арифметические операции запрещены. А именно: нельзя делить и умножать указатели, суммировать два указателя, выполнять над указателями побитовые операции, суммировать указатель со значениями, имеющими тип float или double и т.д. Сравнение указателей Стандартом С допускается сравнение двух указателей. Например, если объявлены два указателя р и q, то следующий оператор является правильным: if(p < q) printf("p ссылается на меньший адрес, чем q\n"); Как правило, сравнение указателей может оказаться полезным, только тогда, когда два указателя ссылаются на общий объект, например, на массив. В качестве примера рассмотрим программу с двумя стековыми функциями, предназначенными для записи и считывания целых чисел. Стек — это список, использующий систему доступа "первым вошел — последним вышел". Иногда стек сравнивают со стопкой тарелок на столе: первая, поставленная на стол, будет взята последней. Стеки часто используются в компиляторах, интерпретаторах, программах обработки крупноформатных таблиц и в других системных программах. Для создания стека необходимы две функции: push() и pop(). Функция push() заносит числа в стек, a pop() — извлекает их. В данном примере эти функции используются в main(). При вводе числа с клавиатуры, программа помещает его в стек. Если ввести 0, то число извлекается из стека. Программа завершает работу при вводе -1. #include #include #define SIZE 50 void push(int i); int pop(void); int *tos, *p1, stack[SIZE]; int main(void) { int value; tos = stack; /* tos ссылается на основание стека */ p1 = stack; /* инициализация p1 */ do { printf("Введите значение: "); scanf("%d", &value); if(value != 0) push(value); else printf("значение на вершине равно %d\n", pop()); } while(value != -1); return 0; } void push(int i) { p1++; if(p1 == (tos+SIZE)) { printf("Переполнение стека.\n"); exit(1); } *p1 = i; } int pop(void) { if(p1 == tos) { printf("Стек пуст.\n"); exit(1); } p1--; return *(p1+1); } Стек хранится в массиве stack. Сначала указатели p1 и tos устанавливаются на первый элемент массива stack. В дальнейшем p1 ссылается на верхний элемент стека, a tos продолжает хранить адрес основания стека. После инициализации стека используются функции push() и pop(). Они выполняют запись в стек и считывание из него, проверяя каждый раз соблюдение границы стека. В функции push() проверяется, что указатель p1 не превышает верхней границы стека tos+SIZE. Это предотвращает переполнение стека. В функции pop() проверяется, что указатель p1 не выходит за нижнюю границу стека. В операторе return функции pop() скобки необходимы потому, что без них оператор return *p1+1; вернул бы значение, расположенное по адресу p1, увеличенное на 1, а не значение по адресу p1+1. Указатели и массивы Понятия указателей и массивов тесно связаны. Рассмотрим следующий фрагмент программы: char str[80], *p1; p1 = str; Здесь p1 указывает на первый элемент массива str. Обратиться к пятому элементу массива str можно с помощью любого из двух выражений: str[4] * (p1+4) Массив начинается с нуля. Поэтому для пятого элемента массива str нужно использовать индекс 4. Можно также увеличить p1 на 4, тогда он будет указывать на пятый элемент. (Напомним, что имя массива без индекса возвращает адрес первого элемента массива.) В языке С существуют два метода обращения к элементу массива: адресная арифметика и индексация массива. Стандартная запись массивов с индексами наглядна и удобна в использовании, однако с помощью адресной арифметики иногда удается сократить время доступа к элементам массива. Поэтому адресная арифметика часто используется в программах, где существенную роль играет быстродействие. В следующем фрагменте программы приведены две версии функции putstr(), выводящей строку на экран. В первой версии используется индексация массива, а во второй — адресная арифметика: /* Индексация указателя s как массива. */ void putstr(char *s) { register int t; for(t=0; s[t]; ++t) putchar(s[t]); } /* Использование адресной арифметики. */ void putstr(char *s) { while(*s) putchar(*s++); } Большинство профессиональных программистов сочтут вторую версию более наглядной и удобной. Для большинства компиляторов она также более быстродействующая. Поэтому в процедурах такого типа приемы адресной арифметики используются довольно часто. Массивы указателей Как и объекты любых других типов, указатели могут быть собраны в массив. В следующем операторе объявлен массив из 10 указателей на объекты типа int: int *x[10]; Для присвоения, например, адреса переменной var третьему элементу массива указателей, необходимо написать: x[2] = &var; В результате этой операции, следующее выражение принимает то же значение, что и var: *x[2] Для передачи массива указателей в функцию используется тот же метод, что и для любого другого массива: имя массива без индекса записывается как формальный параметр функции. Например, следующая функция может принять массив x в качестве аргумента: void display_array(int *q[]) { int t; for(t=0; t<10; t++) printf("%d ", *q[t]); } Необходимо помнить, что q — это не указатель на целые, а указатель на массив указателей на целые. Поэтому параметр q нужно объявить как массив указателей на целые. Нельзя объявить q просто как указатель на целые, потому что он представляет собой указатель на указатель. Массивы указателей часто используются при работе со строками. Например, можно написать функцию, выводящую нужную строку с сообщением об ошибке по индексу num: void syntax_error(int num) { static char *err[] = { "Нельзя открыть файл\n", "Ошибка при чтении\n", "Ошибка при записи\n", "Некачественный носитель\n" }; printf("%s", err[num]); } Массив err содержит указатели на строки с сообщениями об ошибках. Здесь строковые константы в выражении инициализации создают указатели на строки. Аргументом функции printf() служит один из указателей массива err, который в соответствии с индексом num указывает на нужную строку с сообщением об ошибке. Например, если в функцию syntax_error() передается num со значением 2, то выводится сообщение Ошибка при записи. Аргумент командной строки argv также является массивом указателей на строковые константы. Многоуровневая адресация Иногда указатель может ссылаться на указатель, который ссылается на число. Это называется многоуровневой адресацией. Иногда применение таких указателей существенно усложняет программу, делает ее плохо читаемой и подверженной ошибкам. Рис. 5.3 иллюстрирует концепцию многоуровневой адресации. На рисунке видно, что значением "нормального" указателя является адрес объекта, содержащего нужное значение. В случае двухуровневой адресации первый указатель содержит адрес второго указателя, который содержит адрес объекта с нужным значением. Многоуровневая адресация может иметь сколько угодно уровней, однако уровни глубже второго, т.е. указатели более глубокие, чем "указатели на указатели" применяются крайне редко. Дело в том, что при использовании таких указателей часто встречаются концептуальные ошибки из-за того, что смысл таких указателей представить трудно. Не следует путать многоуровневую адресацию с многоуровневыми структурами данных, использующими указатели, такими, например, как связные списки. Это фундаментально различные концепции. Переменная, являющаяся указателем на указатель, должна быть соответствующим образом объявлена. Это делается с помощью двух звездочек перед именем переменной. Например, в следующем операторе newbalance объявлена как указатель на указатель на переменную типа float: float **newbalance; Следует хорошо понимать, что newbalance — это не указатель на число типа float, а указатель на указатель на число типа float. Одноуровневая и многоуровневая адресация Указатель Переменная +--------+ +--------+ | Адрес |------->|Значение| +--------+ +--------+ Одноуровневая адресация Указатель Указатель Переменная +--------+ +--------+ +--------+ | Адрес |----->| Адрес |----->|Значение| +--------+ +--------+ +--------+ Многоуровневая адресаци При двухуровневой адресации для доступа к значению объекта нужно поставить перед идентификатором две звездочки: #include int main(void) { int x, *p, **q; x = 10; p = &x; q = &p; printf("%d", **q); /* печать значения x */ return 0; } Здесь p объявлена как указатель на целое, a q — как указатель на указатель на целое. Функция printf() выводит на экран число 10. Инициализация указателей После объявления нестатического локального указателя до первого присвоения он содержит неопределенное значение. (Глобальные и статические локальные указатели при объявлении неявно инициализируются нулем.) Если попытаться использовать указатель перед присвоением ему нужного значения, то скорее всего он мгновенно разрушит программу или всю операционную систему. Это очень досадная ошибка. При работе с указателями большинство программистов придерживаются следующего важного соглашения: указатель, не ссылающийся в текущий момент времени должным образом на конкретный объект, должен содержать нулевое значение. Нуль используется потому, что С гарантирует отсутствие чего-либо по нулевому адресу. Следовательно, если указатель равен нулю, то это значит, во-первых, что он ни на что не ссылается, а во-вторых — что его сейчас нельзя использовать. Указателю можно задать нулевое значение, присвоив ему 0. Например, следующий оператор инициализирует р нулем: char *p = 0; Дополнительно к этому во многих заголовочных файлах языка С, например, в определен макрос NULL, являющийся нулевой указательной константой. Поэтому в программах на С часто можно увидеть следующее присваивание: p = NULL; Однако равенство указателя нулю не делает его абсолютно "безопасным". Использование нуля в качестве признака неподготовленности указателя — это только соглашение программистов, но не правило языка С. В следующем примере компиляция пройдет без ошибки, а результат, тем не менее, будет неправильным: int *p = 0; *p = 10; /* ошибка! */ В этом случае присваивание посредством p будет присваиванием по нулевому адресу, что обычно вызывает разрушение программы. Во многих процедурах для повышения эффективности программы можно использовать то, что нулевой указатель заведомо считается неподготовленным для использования. Например, можно использовать нулевой указатель как признак конца массива указателей (по аналогии с нулевым терминатором строки). Процедура, использующая массив указателей, таким образом узнает о конце массива. Такой подход иллюстрируется в таком примере. Просматривая список имен, функция search() определяет, есть ли в этом списке заданное имя. #include #include int search(char *p[], char *name); char *names[] = { "Сергей", "Юрий", "Ольга", "Игорь", NULL}; /* Нулевая константа кончает список */ int main(void) { if(search(names, "Ольга") != -1) printf("Ольга есть в списке.\n"); if(search(names, "Павел") == -1) printf("Павел в списке не найден.\n"); return 0; } /* Просмотр имен. */ int search(char *p[], char *name) { register int t; for(t=0; p[t]; ++t) if(!strcmp(p[t], name)) return t; return -1; /* имя не найдено */ } В функцию search() передаются два параметра. Первый из них, p — массив указателей на строки, представляющие собой имена из списка. Второй параметр name является указателем на строку с заданным именем. Функция search() просматривает массив указателей, пока не найдет строку, совпадающую со строкой, на которую указывает name. Итерации цикла for повторяются до тех пор, пока не произойдет совпадение имен, или не встретится нулевой указатель. Конец массива отмечен нулевым указателем, поэтому при достижении конца массива управляющее условие цикла примет значение ЛОЖЬ. Иными словами, p[t] имеет значение ЛОЖЬ, когда p[t] является нулевым указателем. В рассмотренном примере именно это и происходит, когда идет поиск имени "Павел", которого в списке нет. В программах на С указатель типа char * часто инициализируют строковой константой: char *p = "тестовая строка"; Переменная р является указателем, а не массивом. Поэтому возникает логичный вопрос: где хранится строковая константа "тестовая строка"? Так как p не является массивом, она не может храниться в p, тем не менее, она где-то записана. Чтобы ответить на этот вопрос, нужно знать, что происходит, когда компилятор встречает строковую константу. Компилятор создает так называемую таблицу строк, в ней он сохраняет строковые константы, которые встречаются ему по ходу чтения текста программы. Следовательно, когда встречается объявление с инициализацией, компилятор сохраняет строку "тестовая строка" в таблице строк, а в указатель p записывает ее адрес. Дальше в программе указатель p может быть использован как любая другая строка. Это иллюстрируется следующим примером: #include #include char *p = "тестовая строка"; int main(void) { register int t; /* печать строки слева направо и справа налево */ printf(p); for(t=strlen(p)-1; t>-1; t--) printf("%c", p[t]); return 0; } Указатели на функции Указатели на функции — очень мощное средство языка С. Хотя нельзя не отметить, что это весьма трудный для понимания термин. Функция располагается в памяти по определенному адресу, который можно присвоить указателю в качестве его значения. Адресом функции является ее точка входа. Именно этот адрес используется при вызове функции. Так как указатель хранит адрес функции, то она может быть вызвана с помощью этого указателя. Он позволяет также передавать ее другим функциям в качестве аргумента. В программе на С адресом функции служит ее имя без скобок и аргументов (это похоже на адрес массива, который равен имени массива без индексов). Рассмотрим следующую программу, в которой сравниваются две строки, введенные пользователем. Обратите внимание на объявление функции check() и указатель p внутри main(). Указатель p является указателем на функцию. #include #include void check(char *a, char *b, int (*cmp)(const char *, const char *)); int main(void) { char s1[80], s2[80]; int (*p)(const char *, const char *); /* указатель на функцию */ p = strcmp; /* присваивает адрес функции strcmp указателю p */ printf("Введите две строки.\n"); gets(s1); gets(s2); check(s1, s2, p); /* Передает адрес функции strcmp посредством указателя p */ return 0; } void check(char *a, char *b, int (*cmp)(const char *, const char *)) { printf("Проверка на совпадение.\n"); if(!(*cmp)(a, b)) printf("Равны"); else printf("Не равны"); } Проанализируем эту программу подробно. В первую очередь рассмотрим объявление указателя p в main(): int (*p)(const char *, const char *); Это объявление сообщает компилятору, что p — это указатель на функцию, имеющую два параметра типа const char * и возвращающую значение типа int. Скобки вокруг p необходимы для правильной интерпретации объявления компилятором. Подобная форма объявления используется также для указателей на любые другие функции, нужно лишь внести изменения в зависимости от возвращаемого типа и параметров функции. Теперь рассмотрим функцию check(). В ней объявлены три параметра: два указателя на символьный тип (a и b) и указатель на функцию cmp. Указатель функции cmp объявлен в том же формате, что и p. Поэтому в cmp можно хранить значение указателя на функцию, имеющую два параметра типа const char * и возвращающую значение int. Как и в объявлении p, круглые скобки вокруг *cmp необходимы для правильной интерпретации этого объявления компилятором. Вначале в программе указателю p присваивается адрес стандартной библиотечной функции strcmp(), которая сравнивает строки. Потом программа просит пользователя ввести две строки и передает указатели на них функции check(), которая их сравнивает. Внутри check() выражение (*cmp)(a, b) вызывает функцию strcmp(), на которую указывает cmp, с аргументами a и b. Скобки вокруг *cmp обязательны. Существует и другой, более простой, способ вызова функции с помощью указателя: cmp(a, b); Однако первый способ используется чаще (и мы рекомендуем использовать именно его), потому что при втором способе вызова указатель cmp очень похож на имя функции, что может сбить с толку читающего программу. В то же время у первого способа записи есть свои преимущества, например, хорошо видно, что функция вызывается с помощью указателя на функцию, а не имени функции. Первоначально в С был определен именно первый способ вызова. Вызов функции check() можно записать, используя непосредственно имя strcmp(): check(s1, s2, strcmp); В этом случае вводить в программу дополнительный указатель p нет необходимости. У читателя может возникнуть вопрос: какая польза от вызова функции с помощью указателя на функцию? Ведь в данном случае никаких преимуществ не достигнуто, этим мы только усложнили программу. Тем не менее, во многих случаях оказывается более выгодным передать имя функции как параметр или даже создать массив функций. Например, в программе интерпретатора синтаксический анализатор (программа, анализирующая выражения) часто вызывает различные вспомогательные функции, такие как вычисление математических функций, процедуры ввода-вывода и т.п. В таких случаях чаще всего создают список функций и вызывают их с помощью индексов. Альтернативный подход — использование оператора switch с длинным списком меток case — делает программу более громоздкой и подверженной ошибкам. В следующем примере рассматривается расширенная версия предыдущей программы. В этой версии функция check() устроена так, что может выполнять разные операции над строками s1 и s2 (например, сравнивать каждый символ с соответствующим символом другой строки или сравнивать числа, записанные в строках) в зависимости от того, какая функция указана в списке аргументов. Например, строки "0123" и "123" отличаются, однако представляют одно и то же числовое значение. #include #include #include #include void check(char *a, char *b, int (*cmp)(const char *, const char *)); int compvalues(const char *a, const char *b); int main(void) { char s1[80], s2[80]; printf("Введите два значения или две строки.\n"); gets(s1); gets(s2); if(isdigit(*s1)) { printf("Проверка значений на равенство.\n"); check(s1, s2, compvalues); } else { printf("Проверка строк на равенство.\n"); check(s1, s2, strcmp); } return 0; } void check(char *a, char *b, int (*cmp)(const char *, const char *)) { if(!(*cmp)(a, b)) printf("Равны"); else printf("Не равны"); } int compvalues(const char *a, const char *b) { if(atoi(a)==atoi(b)) return 0; else return 1; } Если в этом примере ввести первый символ первой строки как цифру, то check() использует compvalues(), в противном случае — strcmp(). Функция check() вызывает ту функцию, имя которой указано в списке аргументов при вызове check(), поэтому она в разных ситуациях может вызывать разные функции. Ниже приведены результаты работы этой программы в двух случаях: Введите два значения или две строки. тест тест Проверка строк на равенство. Равны Введите два значения или две строки. 0123 123 Проверка значений на равенство. Равны Сравнение строк 0123[2] и 123 показывает равенство их значений. Функции динамического распределения Указатели используются для динамического выделения памяти компьютера для хранения данных. Динамическое распределение означает, что программа выделяет память для данных во время своего выполнения. Память для глобальных переменных выделяется во время компиляции, а для нестатических локальных переменных — в стеке. Во время выполнения программы ни глобальным, ни локальным переменным не может быть выделена дополнительная память. Но довольно часто такая необходимость возникает, причем объем требуемой памяти заранее неизвестен. Такое случается, например, при использовании динамических структур данных, таких как связные списки или двоичные деревья. Такие структуры данных при выполнении программы расширяются или сокращаются по мере необходимости. Для реализации таких структур в программе нужны средства, способные по мере необходимости выделять и освобождать для них память. Память, выделяемая в С функциями динамического распределения данных, находится в т.н. динамически распределяемой области памяти (heap). Динамически распределяемая область памяти — это свободная область памяти, не используемая программой, операционной системой или другими программами. Размер динамически распределяемой области памяти заранее неизвестен, но как правило в ней достаточно памяти для размещения данных программы. Большинство компиляторов поддерживают библиотечные функции, позволяющие получить текущий размер динамически распределяемой области памяти, однако эти функции не определены в Стандарте С. Хотя размер динамически распределяемой области памяти очень большой, все же она конечна и может быть исчерпана. Основу системы динамического распределения в С составляют функции malloc() и free(). Эти функции работают совместно. Функция malloc() выделяет память, а free() — освобождает ее. Это значит, что при каждом запросе функция malloc() выделяет требуемый участок свободной памяти, a free() освобождает его, то есть возвращает системе. В программу, использующую эти функции, должен быть включен заголовочный файл . Прототип функции malloc() следующий: void *malloc(size_t количество_байтов); Здесь количество_байтов — размер памяти, необходимой для размещения данных. (Тип size_t определен в как некоторый целый без знака.) Функция malloc() возвращает указатель типа void *, поэтому его можно присвоить указателю любого типа. При успешном выполнении malloc() возвращает указатель на первый байт непрерывного участка памяти, выделенного в динамически распределяемой области памяти. Если в динамически распределяемой области памяти недостаточно свободной памяти для выполнения запроса, то память не выделяется и malloc() возвращает нуль. При выполнении следующего фрагмента программы выделяется непрерывный участок памяти объемом 1000 байтов: char *p; p = malloc(1000); /* выделение 1000 байтов */ После присвоения указатель p ссылается на первый из 1000 байтов выделенного участка памяти. В следующем примере выделяется память для 50 целых. Для повышения мобильности (переносимости программы с одной машины на другую) используется оператор sizeof. int *p; p = malloc(50*sizeof(int)); Поскольку динамически распределяемая область памяти не бесконечна, при каждом размещении данных необходимо проверять, состоялось ли оно. Если malloc() не смогла по какой-либо причине выделить требуемый участок памяти, то она возвращает нуль. В следующем примере показано, как выполняется проверка успешности размещения: p = malloc(100); if(!p) { printf("Нехватка памяти.\n"); exit(1); } Конечно, вместо выхода из программы exit() можно поставить какой-либо обработчик ошибки. Обязательным здесь можно назвать лишь требование не использовать указатель р, если он равен нулю. Функция free() противоположна функции malloc() в том смысле, что она возвращает системе участок памяти, выделенный ранее с помощью функции malloc(). Иными словами, она освобождает участок памяти, который может быть вновь использован функцией malloc(). Функция free() имеет следующий прототип: void free(void *p) Здесь р — указатель на участок памяти, выделенный перед этим функцией malloc(). Функцию free() ни в коем случае нельзя вызывать с неправильным аргументом, это мгновенно разрушит всю систему распределения памяти. Подсистема динамического распределения в С используется совместно с указателями для создания различных программных конструкций, таких как связные списки и двоичные деревья. Несколько примеров использования таких конструкций приведены в части IV. Здесь рассматривается другое важное применение динамического размещения: размещение массивов. Динамическое выделение памяти для массивов Довольно часто возникает необходимость выделить память динамически, используя malloc(), но работать с этой памятью удобнее так, будто это массив, который можно индексировать. В этом случае нужно создать динамический массив. Сделать это несложно, потому что каждый указатель можно индексировать как массив. В следующем примере одномерный динамический массив содержит строку: /* Динамическое распределение строки, строка вводится пользователем, а затем распечатывается справа налево. */ #include #include #include int main(void) { char *s; register int t; s = malloc(80); if(!s) { printf("Требуемая память не выделена.\n"); exit(1); } gets(s); for(t=strlen(s)-1; t>=0; t--) putchar(s[t]); free(s); return 0; } Перед первым использованием s программа проверяет, успешно ли прошло выделение памяти. Эта проверка необходима для предотвращения случайного использования нулевого указателя. Указатель s используется в функции gets(), а также при выводе на экран (но на этот раз уже как обыкновенный массив). Можно также динамически выделить память для многомерного массива. Для этого нужно объявить указатель, определяющий все, кроме самого левого измерения массива. В следующем примере двухмерный динамический массив содержит таблицу чисел от 1 до 10 в степенях 1, 2, 3 и 4. #include #include int pwr(int a, int b); int main(void) { /* Объявление указателя на массив из 10 строк в которых хранятсяцелые числа (int). */ int (*p)[10]; register int i, j; /* выделение памяти для массива 4 x 10 */ p = malloc(40*sizeof(int)); if(!p) { printf("Требуемая память не выделена.\n"); exit(1); } for(j=1; j<11; j++) for(i=1; i<5; i++) p[i-1][j-1] = pwr(j, i); for(j=1; j<11; j++) { for(i=1; i<5; i++) printf("%10d ", p[i-1][j-1]); printf("\n"); } return 0; } /* Возведение чисел в степень. */ pwr(int a, int b) { register int t=1; for(; b; b--) t = t*a; return t; } Программа выводит на экран следующее: 1 1 1 1 2 4 8 16 3 9 27 81 4 16 64 256 5 25 125 625 6 36 216 1296 7 49 343 2401 8 64 512 4096 9 81 729 6561 10 100 1000 10000 Указатель р в главной программе (main()) объявлен как int (*p)[10] Скобки вокруг *р обязательны. Такое объявление означает, что р указывает на массив из 10 целых. Если увеличить указатель р на 1, то он будет указывать на следующие 10 целых чисел. Таким образом, р — это указатель на двухмерный массив с 10 числами в каждой строке. Поэтому р можно индексировать как обычный двухмерный массив. Разница только в том, что здесь память выделена с помощью malloc(), а для обыкновенного массива память выделяет компилятор. В C++ нужно преобразовывать типы указателей явно. Поэтому чтобы данная программа была правильной и в С, и в C++, необходимо выполнить явное приведение типа значения, возвращаемого функцией malloc(). Для этого строчку, в которой указателю р присваивается это значение, нужно переписать следующим образом: p = (int (*)[10]) malloc(40*sizeof(int)); Многие программисты используют явное преобразование типов указателей для обеспечения совместимости с C++. Указатели с квалификатором restrict Стандарт С99 дополнительно вводит новый квалификатор типа restrict, применимый только для указателей. Подробно этот спецификатор обсуждается в части II, здесь приведено только его краткое описание. Если указатель объявлен с квалификатором restrict, то к объекту, на который он ссылается, можно обратиться только с помощью этого указателя. Обращение к объекту с помощью другого указателя возможно только в том случае, если другой указатель основан на первом. Таким образом, доступ к объекту можно получить только с помощью выражений, основанных на указателе с квалификатором restrict. Указатели restrict используются главным образом как параметры функции или совместно с malloc(). Если указатель объявлен с квалификатором restrict, компилятор способен лучше оптимизировать некоторые процедуры. Например, если два параметра функции определены как указатели с квалификатором restrict, то это сообщает компилятору о том, что они указывают на два разных (не пересекающихся) объекта. Квалификатор restrict не изменяет семантику программы. Трудности при работе с указателями Ничто не может доставить больше неприятностей, чем "дикий" указатель! Указатели похожи на обоюдоострое оружие: их возможности огромны, однако обезвредить ошибки в них особенно трудно. Ошибочный указатель трудно найти потому, что ошибка в самом указателе никак себя не проявляет. Проблемы возникают при попытке обратиться к объекту с помощью этого указателя. Если значение указателя неправильное, то программа с его помощью обращается к произвольной ячейке памяти. При чтении в программу попадают неправильные данные, а при записи искажаются другие данные, хранящиеся в памяти, или портится участок программы, не имеющий никакого отношения к ошибочному указателю. В обоих случаях ошибка может не проявиться вовсе или проявиться позже в форме, никак не указывающей на ее причину. Поскольку ошибки, связанные с указателями, особенно трудно обезвредить, при работе с указателями следует соблюдать особую осторожность. Рассмотрим некоторые ошибки, наиболее часто возникающие при работе с указателями. Классический пример — неинициализированный указатель: /* Это программа содержит ошибку. */ int main(void) { int x, *p; x = 10; *p = x; /* ошибка, p не инициализирован */ return 0; } Эта программа присваивает значение 10 некоторой неизвестной области памяти. Рассмотрим, почему это происходит. Хотя указателю р не было присвоено никакого значения, но в момент выполнения операции *р = х он имел некоторое (совершенно произвольное!) значение. Поэтому здесь имела место попытка выполнить операцию записи в область памяти, на которую указывал данный указатель. В небольших программах такая ошибка часто остается незамеченной, потому что если программа и данные занимают немного места, то "выстрел наугад" скорее всего будет "промахом". С увеличением размера программы вероятность "попасть" в нее возрастает. В таком простом случае большинство компиляторов выводят предупреждение о том, что используется неинициализированный указатель. Однако подобная ошибка может произойти и в более завуалированном виде, тогда компилятор не сможет распознать ее. Вторая распространенная ошибка заключается в простом недоразумении при использовании указателя: /* Это программа содержит ошибку. */ #include int main(void) { int x, *p; x = 10; p = x; printf("%d", *p); return 0; } Вызов printf() не выводит на экран значение х, равное 10. Выводится произвольная величина, потому что оператор p = x; записан неправильно. Он присваивает значение 10 указателю, однако указатель должен содержать адрес, а не значение. Правильный оператор выглядит так: p = &x; Большинство компиляторов при попытке присвоить указателю р значение х выведут предупреждающее сообщение, но, как и в предыдущем примере, компилятор не сможет распознать эту ошибку в более завуалированном виде. Еще одна типичная ошибка происходит иногда при неправильном понимании принципов расположения переменных в памяти. Программисту ничего не известно о том, как используемые им данные располагаются в памяти, будут ли они расположены так же при следующем выполнении программы или как их расположат другие компиляторы. Поэтому сравнивать одни указатели с другими недопустимо. Например, программа char s[80], y[80]; char *p1, *p2; p1 = s; p2 = y; if(p1 < p2) . . . в общем случае неправильна. (В некоторых необычных ситуациях иногда определяют относительное положение переменных, но это делают очень редко.) Похожая ошибка возникает, когда делается необоснованное предположение о расположении массивов. Иногда, предполагая, что массивы расположены рядом, пытаются обращаться к ним с помощью одного и того же указателя, например: int first[10], second[10]; int *p, t; p = first; for(t=0; t<20; ++t) *p++ = t; Так присваивать значения массивам first и second нельзя. Если компилятор разместит массивы рядом, это может и не привести к неправильному результату. Однако подобная ошибка особенно неприятна тем, что при проверке она может остаться незамеченной, а потом компилятор будет размещать массивы по-другому и программа выполнится неправильно. В следующей программе приведен пример очень опасной ошибки. Постарайтесь сами найти ее, не подсматривая в последующее объяснение. /* Это программа с ошибкой. */ #include #include int main(void) { char *p1; char s[80]; p1 = s; do { gets(s); /* чтение строки */ /* печать десятичного эквивалента каждого символа */ while(*p1) printf(" %d", *p1++); } while(strcmp(s, "выполнено")); return 0; } Программа печатает значения символов ASCII, находящихся в строке s. Печать осуществляется с помощью p1, указывающего на s. Ошибка состоит в том, что указателю p1 присвоено значение s только один раз, перед циклом. В первой итерации p1 правильно проходит по символам строки s, однако в следующей итерации он начинает не с первого символа, а с того, которым закончил в предыдущей итерации. Так что во второй итерации p1 может указывать на середину второй строки, если она длиннее первой, или же вообще на конец остатка первой строки. Исправленная версия программы записывается так: /* Это правильная программа. */ #include #include int main(void) { char *p1; char s[80]; do { p1 = s; /* установка p1 в начало строки s */ gets(s); /* чтение строки */ /* печать десятичного эквивалента каждого символа */ while(*p1) printf(" %d", *p1++); } while(strcmp(s, "выполнено")); return 0; } При такой записи указатель p1 в начале каждой итерации устанавливается на первый символ строки s. Об этом необходимо всегда помнить при повторном использовании указателей. То, что неправильные указатели могут быть очень "коварными", не может служить причиной отказа от их использования. 24 Строки: определение, инициализация, функции для работы со строками. Алгоритмы поиска подстроки в строке. Алгоритм Кнута-Морисса-Пратта Строки Одномерный массив наиболее часто применяется в виде строки символов. Строка — это одномерный массив символов, заканчивающийся нулевым символом. В языке С признаком окончания строки (нулевым символом) служит символ '\0'. Таким образом, строка содержит символы, составляющие строку, а также нулевой символ. Это единственный вид строки, определенный в С. В C++ дополнительно определен специальный класс строк, называющийся String, который позволяет обрабатывать строки объектно-ориентированными методами. Стандарт С не поддерживает String. Объявляя массив символов, предназначенный для хранения строки, необходимо предусмотреть место для нуля, т.е. указать его размер в объявлении на один символ больше, чем наибольшее предполагаемое количество символов. Например, объявление массива str, предназначенного для хранения строки из 10 символов, должно выглядеть так: char str[11]; Последний, 11-й байт предназначен для нулевого символа. Записанная в тексте программы строка символов, заключенных в двойные кавычки, является строковой константой, например, "некоторая строка" В конец строковой константы компилятор автоматически добавляет нулевой символ. Для обработки строк в С определено много различных библиотечных функций. Чаще всего используются следующие функции: Имя функции Выполняемое действие strcpy(s1,s2) Копирование s2 в s1 strcat(s1,s2) Конкатенация (присоединение) s2 в конец s1 strlen(s1) Возвращает длину строки s1 strcmp(s1,s2) Возвращает 0, если s1 и s2 совпадают, отрицательное значение, если s1s2 strchr(s1,ch) Возвращает указатель на первое вхождение символа ch в строку s1 strstr(s1,s2) Возвращает указатель на первое вхождение строки s2 в строку s1 Эти функции объявлены в заголовочном файле . Применение библиотечных функций обработки строк иллюстрируется следующим примером: #include #include int main(void) { char s1[80], s2[80]; gets(s1); gets(s2); printf("Длина: %d %d\n", strlen(s1), strlen(s2)); if(!strcmp(s1, s2)) printf("Строки равны\n"); strcat(s1, s2); printf("%s\n", s1); strcpy(s1, "Проверка.\n"); printf(s1); if(strchr("Алло", 'e')) printf(" л есть в Алло\n"); if(strstr("Привет", "ив")) printf(" найдено ив "); return 0; } Если эту программу выполнить и ввести в s1 и в s2 одну и ту же строку "Алло!", то на экран будет выведено следующее: Длина: 5 5 Строки равны Алло!Алло! Проверка, л есть в Алло найдено ив strcmp() принимает значение ЛОЖЬ, если строки совпадают (хоть это и несколько нелогично). Поэтому в тесте на совпадение нужно использовать логический оператор отрицания. Линейный метод поиска подстроки в строке В данном методе последовательно, начиная с первого символа исходной строки, в промежуточную переменную строкового типа копируются подстроки. Копируемые подстроки по длине должны совпадать с длиной искомой подстроки. Если копируемая подстрока совпадает с искомой, то совпадение найдено. Если же совпадение не обнаружено, то копируется подстрока, входящая в исходную строку, начиная со второго символа. И т.д. до тех пор, пока не будет найдено совпадение или же просмотрена вся строка и совпадение не найдено. Очевидно, что последнее совпадение возможно при копировании подстроки, начиная с символа, номер позиции которого в строке равен (длина_строки-длина_подстроки +1). Алгоритм ПоискаПодстрокиВСтроке; Вход S {строка}; P {подстрока, которую ищем} Выход B {результат поиска, логическая перемен.} Локальные переменные S1 {вспом. переменные, строка} imax, i {вспом. переменные, целые числа} Начало N:=length(s);{длина строки} M:=length(p);{длина подстроки} Imax:=n-m+1; i:=0; Повторять Нц i:=i+1; s1:=copy(s,i,m); b:=(s1=p); Кц До В или (i=max); Конец; Алгоритм Кнута-Морриса_Пратта. Постановка задачи о точном совпадении Пусть задана строка Р, именуемая образцом или паттерном, и более длинная строка S, именуемая текстом. Задача о точном совпадении заключается в отыскании всех вхождений образца Р в текст S. Например, если Р=aba и S=bbabaxababay, то Р входит в S, начиная с позиций 3, 7 и 9. Два вхождения могут перекрываться, как это видно по вхождения Р в позициях 7 и 9. Задача о точном совпадении возникает в широком спектре приложений: • в текстовых редакторах; • в утилитах типа GREP; • в информационно-поисковых системах; • в интерактивных энциклопедиях, словарях и тезаурусах; • в специализированных базах данных • и т.п. У многих алгоритмов сравнения и анализа строк эффективность сильно возрастает из-за пропусков сравнений. Эти пропуски получаются благодаря изучению внутренней структуры либо образца, либо текста S. При этом другая строка может оставаться неизвестной алгоритму. Эта часть алгоритма называется препроцессной фазой. За ней следует фаза поиска, на которой информация, полученная в препроцессной фазе используется для сокращения работы по поиску вхождений Р в S. Самый известный алгоритм с линейным временем для задачи точного совпадения предложен Кнутом, Моррисом и Праттом. Идея сдвига Кнута – Морриса – Пратта Предположим, что при некотором выравнивании Р около S обнаружено совпадение первых j символов из Р с их парами из S, а при следующем сравнении было несовпадение. Например, если Р = abcxabcde и при сравнении Р с S нашли несовпадение в позиции 8 подстроки Р, то есть возможность сдвинуть Р на четыре символа без пропуска вхождений Р в S. Это можно увидеть, ничего не зная о тексте S и расположения Р относительно S. Требуется знать только место несовпадения Р. Алгоритм Кнута – Морриса – Пратта, основываясь на таком способе рассуждений, и делает этот сдвиг больше. Препроцессинг для метода КМП Препроцессная фаза метода КМП состоит в исследовании подстроки. Для каждого символа Р определяется значение наибольшей длины последовательности символов (суффикса), совпадающих с началом подстроки (префиксом) – d[j]=len, где j – номер позиции последнего символа суффикса, совпадающего с префиксом в подстроке Р, len – длина совпадающей части в символах. Например: 1Пример. Дана подстрока , тогда d[1]=0; D[2]=0; D[3]=0; D[4]=1; так как четвертый символ совпадает с первым, длина совпадающей последовательности равна 1 символу. D[5]=2; в пятом символе найдено совпадение с двумя первыми символами подстроки, длина совпадающей последовательности равна 2 символам. D[6]=3; при J=6 длина совпадающей последовательности равна трем символам (abc). D[7]=0; 2 Пример. Дана подстрока , тогда d[1]=0; D[2]=0; D[3]=0; D[4]=0; так как четвертых символ не совпадает с первым. D[5]=1; в пятом символе найдено совпадение с первыми символом подстроки, длина совпадающей последовательности равна 1 символу. D[6]=0; D[7]=0; D[8]=0; D[9]=1; 3 Пример. Дана подстрока , тогда d[1]=0; D[2]=0; D[3]=0; D[4]=0; так как четвертых символ не совпадает с первым. D[5]=1; D[6]=2; D[7]=3; D[8]=1; 4 Пример. Дана подстрока , тогда d[1]=0; D[2]=0; D[3]=0; D[4]=1; так как S[1]= s[4]=a D[5]=2; так как S[1..2]= s[4..5]=ab D[6]=3; так как S[1..3]= s[4..6]=abc D[7]=4; так как S[1..4]= s[4..7]=abca D[8]=5; так как S[1..5]= s[4..8]=abcab Если для любого расположения Р и S первое несовпадение (при ходе слева направо) отмечается в (j+1) позиции подстроки Р и позиции (i+1) строки S, то можно осуществить сдвиг подстроки Р на количество символов, равное . Следующим сравниваемыми символами будут: символ подстроки Р и - ый символ строки S. То есть, если в 4 примере найдено несовпадение то , при следующем сравнении будем сравнивать символ подстроки и символ строки. Реализация алгоритма КМП Константы max=100; Пользовательские типы BackFunc = массив [1..max] элементов типа char; TStr=строка длиной 100 символов; Глобальные данные программы s, { строка } p: { подстрока } TStr; d: BackFunc; {массив длин совпадений} n, { длина строки s } m, { длина подстроки p } k: int;{позиция, в которой образец и текст совпали} {Препроцессорная обработка. Заполнение функции возвратов} Алгоритм Препроцессинга Вход p:TStr; m:char Выход d:BackFunc Локальные переменные j,len^char; Начало d[1]:=0; len:=0; {Начинаем заполнение функции возвратов со второго символа образца} j:=2; {Пока текущий символ образца не является последним} Пока j<=m делать нц {Находим повторяющиеся подпоследовательности. Если нашли несовпадение, то запоминаем текущее значение повторений в переменную len} Пока (len>0) и (p[len+1]<>p[j]) Нц len:=d[len]; кц {Ищем количество подряд идущих символов образца, равных его суффиксу.} Если p[len+1]=p[j] то inc(len); все {Запоминаем текущее значение функции возврата для j-ого символа образца} d[j]:=len; {Переходим к следующему символу образца} inc(j); кц Конец ; Алгоритм процедуры Поиска первого вхождения образца в текст Вход d:BackFunc s,p:char[]; n,m:char; Выход k:char Локальные переменные i,len:char; Начало len:=0; i:=1; {Пока не достигли конца строки и не нашли образец в тексте} Пока (i<=n) и (len<>m) делать нц {Пока число совпаденией символов образца с текстом больше нуля и нашли несовпадение следующего символа образца символу текста, то нам необходимо сдвинуть образец на LEN символов вправо, которое определено функцией возврата. При этом гарантирован непропуск какого-либо вхождения образца в текст} Пока (len>0) и (p[len+1]<>s[i]) делать нц len:=d[len]; кц {Если после продвижения на LEN символов вправо следующий символ образца совпадает с символом текста, то количество совпадений увеличиваем на единицу} Если p[len+1]=s[i] то inc(len); все {Переходим к рассмотрению следующего символа текста} inc(i); Кц {Если количество повторений символов образца и текста равно длине образца,то собственно мы нашли то, что искали. Причем место совпадения первого символа образца = (текущая позиция - длина образца), в противном случае-0} Если len=m то i:=i-m иначе i:=0; все {Запоминаем позицию совпадения образца и текста в выходную переменную процедуры} k:=i; Конец 25 Структуры в Си. Массивы структур, вложенные структуры, указатели на структуры В языке С имеется пять способов создания пользовательских типов данных. Пользовательские типы данных можно создать с помощью: структуры — группы переменных, имеющей одно имя и называемой агрегатным типом данных. (Кроме того, еще известны термины соединение (compound) и конгломерат (conglomerate).); объединения, которое позволяет определять один и тот же участок памяти как два или более типов переменных; битового поля, которое является специальным типом элемента структуры или объединения, позволяющим легко получать доступ к отдельным битам; перечисления — списка поименованных целых констант; ключевого слова typedef, которое определяет новое имя для существующего типа. Структуры Структура — это совокупность переменных, объединенных под одним именем. С помощью структур удобно размещать в смежных полях связанные между собой элементы информации. Объявление структуры создает шаблон, который можно использовать для создания ее объектов (то есть экземпляров этой структуры). Переменные, из которых состоит структура, называются членами. (Члены структуры еще называются элементами или полями.) Как правило, члены структуры связаны друг с другом по смыслу. Например, элемент списка рассылки, состоящий из имени и адреса логично представить в виде структуры. В следующем фрагменте кода показано, как объявить структуру, в которой определены поля имени и адреса. Ключевое слово struct сообщает компилятору, что объявляется (еще говорят, "декларируется") структура. struct addr { char name[30]; char street[40]; char city[20]; char state[3]; unsigned long int zip; }; Объявление завершается точкой с запятой, потому что объявление структуры является оператором. Кроме того, тег структуры addr идентифицирует эту конкретную структуру данных и является спецификатором ее типа. В данном случае на самом деле никакая переменная не создается. Всего лишь определяется вид данных. Когда вы объявляете структуру, то определяете агрегатный тип, а не переменную. И пока вы не объявите переменную этого типа, то существовать она не будет. Чтобы объявить переменную (то есть физический объект) типа addr, напишите следующее: struct addr addr_info; В этом операторе объявлена переменная типа addr, которая называется addr_info. Таким образом, addr описывает вид структуры (ее тип), a addr_info является экземпляром (объектом) этой структуры. Когда объявляется переменная-структура, компилятор автоматически выделяет количество памяти, достаточное, чтобы разместить все ее члены.. Расположение в памяти структуры addr_info +------------------------------------------+ |Name (имя) 30 байт | +------------------------------------------+ +-------------------------------------------------+ |Street (улица) 40 байт | +-------------------------------------------------+ +-----------------------------------+ |City (город) 20 байт | +-----------------------------------+ +---------------------+ |State (штат) 3 байта | +---------------------+ +----------------------------+ |Zip (код) 4 байта | +---------------------------- Одновременно с объявлением структуры можно объявить одну или несколько переменных. Например, struct addr { char name[30]; char street[40]; char city[20]; char state[3]; unsigned long int zip; } addr_info, binfo, cinfo; определяет тип структуры, называемый addr, и объявляет переменные этого типа addr_info, binfo и cinfo. Важно понимать, что каждая переменная-структура содержит собственные копии членов структуры. Например, поле zip в binfo отличается от поля zip в cinfo. Изменения в zip из binfo не повлияют на содержимое поля zip, находящегося в cinfo. Если нужна только одна переменная-структура, то тег структуры является лишним. В этом случае наш пример объявления можно переписать следующим образом: struct { char name[30]; char street[40]; char city[20]; char state[3]; unsigned long int zip; } addr_info; В этом случае объявляется одна переменная с именем addr_info, причем ее поля указаны в структуре, которая предшествует этому имени. Общий вид объявления структуры такой: struct тег { тип имя-члена; тип имя-члена; тип имя-члена; . . . } переменные-структуры; причем тег или переменные-структуры могут быть пропущены, но только не оба одновременно. Доступ к членам структуры Доступ к отдельным членам структуры осуществляется с помощью оператора . (который обычно называют оператором точка или оператором доступа к члену структуры). Например, в следующем выражении полю zip в уже объявленной переменной-структуре addr_info присваивается значение ZIP-кода, равное 12345: addr_info.zip = 12345; Этот отдельный член определяется именем объекта (в данном случае addr_info), за которым следует точка, а затем именем самого этого члена (в данном случае zip). В общем виде использование оператора точка для доступа к члену структуры выглядит таким образом: имя-объекта.имя-члена Поэтому, чтобы вывести ZIP-код на экран, напишите следующее: printf("%d", addr_info.zip); Будет выведен ZIP-код, который находится в члене zip переменной-структуры addr_infо. Точно так же в вызове gets() можно использовать массив символов addr_infо.name: gets(addr_info.name); Таким образом, в начало name передается указатель на символьную строку. Так как name является массивом символов, то чтобы получить доступ к отдельным символам в массиве addr_info.name, можно использовать индексы вместе с name. Например, с помощью следующего кода можно посимвольно вывести на экран содержимое addr_info.name: for(t=0; addr_info.name[t]; ++t) putchar(addr_info.name[t]); Индексируется именно name (а не addr_info). Помните, что addr_info — это имя всего объекта-структуры, a name — имя элемента этой структуры. Таким образом, если требуется индексировать элемент структуры, то индекс необходимо указывать после имени этого элемента. Присваивание структур Информация, которая находится в одной структуре, может быть присвоена другой структуре того же типа при помощи единственного оператора присваивания. Нет необходимости присваивать значения каждого члена в отдельности. #include int main(void) { struct { int a; int b; } x, y; x.a = 10; y = x; /* присваение одной структуры другой */ printf("%d", y.a); return 0; } После присвоения в y.a будет храниться значение 10. Массивы структур Структуры часто образуют массивы. Чтобы объявить массив структур, вначале необходимо определить структуру (то есть определить агрегатный тип данных), а затем объявить переменную массива этого же типа. Например, чтобы объявить 100-элементный массив структур типа addr, напишите следующее: struct addr addr_list[100]; Это выражение создаст 100 наборов переменных, каждый из которых организован так, как определено в структуре addr. Чтобы получить доступ к определенной структуре, указывайте имя массива с индексом. Например, чтобы вывести ZIP-код из третьей структуры, напишите следующее: printf("%d", addr_list[2].zip); Как и в других массивах переменных, в массивах структур индексирование начинается с 0. Для справки: чтобы указать определенную структуру, находящуюся в массиве структур, необходимо указать имя этого массива с определенным индексом. А если нужно указать индекс определенного элемента в структуре, то необходимо указать индекс этого элемента. Таким образом, в результате выполнения следующего выражения первому символу члена name, находящегося в третьей структуре из addr_list, присваивается значение 'X'. addr_list[2].name[0] = 'X'; Пример со списком рассылки Чтобы показать, как используются структуры и массивы структур, создается простая программа работы со списком рассылки, и в ее массиве структур будут храниться адреса и связанная с ними информация. Эта информация записывается в следующие поля: name (имя), street (улица), city (город), state (штат) и zip (почтовый код, индекс). Вся эта информация находится в массиве структур типа addr: struct addr { char name[30]; char street[40]; char city[20]; char state[3]; unsigned long int zip; } addr_list[MAX]; Поле zip имеет целый тип unsigned long. Правда, чаще можно встретить хранение почтовых кодов, в которых используются строки символов, потому что этот способ подходит для почтовых кодов, в которых вместе с цифрами используются и буквы (как, например, в Канаде и других странах). Однако в нашем примере почтовый индекс хранится в виде целого числа; это делается для того, чтобы показать использование числового элемента в структуре. Вот main() — первая функция, которая нужна программе: int main(void) { char choice; init_list(); /* инициализация массива структур */ for(;;) { choice = menu_select(); switch(choice) { case 1: enter(); break; case 2: delete(); break; case 3: list(); break; case 4: exit(0); } } return 0; } Функция начинает выполнение с инициализации массива структур, а затем реагирует на выбранный пользователем пункт меню. Функция init_list() готовит массив структур к использованию, обнуляя первый байт поля name каждой структуры массива. (В программе предполагается, что если поле name пустое, то элемент массива не используется.) А вот сама функция init_list(): /* Инициализация списка. */ void init_list(void) { register int t; for(t=0; t4); return c; } Функция enter() подсказывает пользователю, что именно требуется ввести, и сохраняет введенную информацию в следующей свободной структуре. Если массив заполнен, то выводится сообщение Список заполнен. Функция find_free() ищет в массиве структур свободный элемент. /* Ввод адреса в список. */ void enter(void) { int slot; char s[80]; slot = find_free(); if(slot==-1) { printf("\nСписок заполнен"); return; } printf("Введите имя: "); gets(addr_list[slot].name); printf("Введите улицу: "); gets(addr_list[slot].street); printf("Введите город: "); gets(addr_list[slot].city); printf("Введите штат: "); gets(addr_list[slot].state); printf("Введите почтовый код: "); gets(s); addr_list[slot].zip = strtoul(s, '\0', 10); } /* Поиск свободной структуры. */ int find_free(void) { register int t; for(t=0; addr_list[t].name[0] && t=0 && slot < MAX) addr_list[slot].name[0] = '\0'; } И последняя функция, которая требуется программе, — это list(), которая выводит на экран весь список рассылки. Из-за большого разнообразия компьютерных сред язык С не определяет стандартную функцию, которая бы отправляла вывод на принтер. Однако все нужные для этого средства имеются во всех компиляторах С. Возможно, вам самим захочется сделать так, чтобы программа работы со списками могла еще и распечатывать список рассылки. /* Вывод списка на экран. */ void list(void) { register int t; for(t=0; t #include #define MAX 100 struct addr { char name[30]; char street[40]; char city[20]; char state[3]; unsigned long int zip; } addr_list[MAX]; void init_list(void), enter(void); void delete(void), list(void); int menu_select(void), find_free(void); int main(void) { char choice; init_list(); /* инициализация массива структур */ for(;;) { choice = menu_select(); switch(choice) { case 1: enter(); break; case 2: delete(); break; case 3: list(); break; case 4: exit(0); } } return 0; } /* Инициализация списка. */ void init_list(void) { register int t; for(t=0; t4); return c; } /* Ввод адреса в список. */ void enter(void) { int slot; char s[80]; slot = find_free(); if(slot==-1) { printf("\nСписо заполнен"); return; } printf("Введите имя: "); gets(addr_list[slot].name); printf("Введите улицу: "); gets(addr_list[slot].street); printf("Введите город: "); gets(addr_list[slot].city); printf("Введите штат: "); gets(addr_list[slot].state); printf("Введите почтовый индекс: "); gets(s); addr_list[slot].zip = strtoul(s, '\0', 10); } /* Поиск свободной структуры. */ int find_free(void) { register int t; for(t=0; addr_list[t].name[0] && t=0 && slot < MAX) addr_list[slot].name[0] = '\0'; } /* Вывод списка на экран. */ void list(void) { register int t; for(t=0; t /* Определение типа структуры. */ struct struct_type { int a, b; char ch; } ; void f1(struct struct_type parm); int main(void) { struct struct_type arg; arg.a = 1000; f1(arg); return 0; } void f1(struct struct_type parm) { printf("%d", parm.a); } Как видно из этой программы, при объявлении параметров, являющихся структурами, объявление типа структуры должно быть глобальным, чтобы структурный тип можно было использовать во всей программе. Например, если бы struct_type был бы объявлен внутри main(), то этот тип не был бы виден в f1(). При передаче структуры тип аргумента должен совпадать с типом параметра. Для аргумента и параметра недостаточно просто быть физически похожими; должны совпадать даже имена их типов. Например, следующая версия предыдущей программы неправильная и компилироваться не будет. Дело в том, что имя типа для аргумента, используемого при вызове функции f1(), отличается от имени типа ее параметра. /* Эта программа неправильная и при компиляции будут обнаружены ошибки. */ #include /* Определение типа структур. */ struct struct_type { int a, b; char ch; } ; /* Определение структуры, похожей на struct_type, но сдругими именами. */ struct struct_type2 { int a, b; char ch; } ; void f1(struct struct_type2 parm); int main(void) { struct struct_type arg; arg.a = 1000; f1(arg); /* несовпадение типов */ return 0; } void f1(struct struct_type2 parm) { printf("%d", parm.a); } Указатели на структуры В языке С указатели на структуры также официально признаны, как и указатели на любой другой вид объектов. Однако указатели на структуры имеют некоторые особенности, о которых и пойдет речь. Объявление указателя на структуру Как и другие указатели, указатель на структуру объявляется с помощью звездочки *, которую помещают перед именем переменной структуры. Например, для ранее определенной структуры addr следующее выражение объявляет addr_pointer указателем на данные этого типа (то есть на данные типа addr): struct addr *addr_pointer; Использование указателей на структуры Указатели на структуры используются главным образом в двух случаях: когда структура передается функции с помощью вызова по ссылке, и когда создаются связанные друг с другом списки и другие структуры с динамическими данными, работающие на основе динамического размещения. У такого способа, как передача любых (кроме самых простых) структур функциям, имеется один большой недостаток: при выполнении вызова функции, чтобы поместить структуру в стек, необходимы существенные ресурсы. (Вспомните, что аргументы передаются функциям через стек.) Впрочем, для простых структур с несколькими членами эти ресурсы являются не такими уж большими. Но если в структуре имеется большое количество членов или некоторые члены сами являются массивами, то при передаче структур функциям производительность может упасть до недопустимо низкого уровня. Надо передавать не саму структуру, а указатель на нее. Когда функции передается указатель на структуру, то в стек попадает только адрес структуры. В результате вызовы функции выполняются очень быстро. В некоторых случаях этот способ имеет еще и второе преимущество: передача указателя позволяет функции модифицировать содержимое структуры, используемой в качестве аргумента. Чтобы получить адрес переменной-структуры, необходимо перед ее именем поместить оператор &. Например, в следующем фрагменте кода struct bal { float balance; char name[80]; } person; struct bal *p; /* объявление указателя на структуру */ адрес структуры person можно присвоить указателю p: p = &person; Чтобы с помощью указателя на структуру получить доступ к ее членам, необходимо использовать оператор стрелка ->. Вот, например, как можно сослаться на поле balance: p->balance Оператор ->, который обычно называют оператором стрелки, состоит из знака "минус", за которым следует знак "больше". Стрелка применяется вместо оператора точки тогда, когда для доступа к члену структуры используется указатель на структуру. Программа, которая имитирует таймер, выводящий значения часов, минут и секунд: /* Программа-имитатор таймера. */ #include #define DELAY 128000 struct my_time { int hours; int minutes; int seconds; } ; void display(struct my_time *t); void update(struct my_time *t); void delay(void); int main(void) { struct my_time systime; systime.hours = 0; systime.minutes = 0; systime.seconds = 0; for(;;) { update(&systime); display(&systime); } return 0; } void update(struct my_time *t) { t->seconds++; if(t->seconds==60) { t->seconds = 0; t->minutes++; } if(t->minutes==60) { t->minutes = 0; t->hours++; } if(t->hours==24) t->hours = 0; delay(); } void display(struct my_time *t) { printf("%02d:", t->hours); printf("%02d:", t->minutes); printf("%02d\n", t->seconds); } void delay(void) { long int t; /* если надо, можно изменять константу DELAY (задержка) */ for(t=1; thours==24) t->hours = 0; Таким образом, компилятору дается указание взять адрес t (этот адрес указывает на переменную systime из main()) и сбросить значение hours в нуль. Помните, что оператор точка используется для доступа к элементам структуры при работе с самой структурой. А когда используется указатель на структуру, то надо применять оператор стрелка. Массивы и структуры внутри структур Членом структуры может быть или простая переменная, например, типа int или double, или составной (не скалярный) тип. В языке С составными типами являются массивы и структуры. Один составной тип вы уже видели — это символьные массивы, которые использовались в addr. Члены структуры, которые являются массивами, можно считать такими же членами структуры. struct x { int a[10][10]; /* массив 10 x 10 из целых значений */ float b; } y; Целый элемент с индексами 3, 7 из массива a, находящегося в структуре y, обозначается таким образом: y.a[3][7] Когда структура является членом другой структуры, то она называется вложенной. Например, в следующем примере структура address вложена в emp: struct emp { struct addr address; /* вложенная структура */ float wage; } worker; Здесь структура была определена как имеющая два члена. Первым является структура типа addr, в которой находится адрес работника. Второй член — это wage, где находятся данные по его зарплате. В следующем фрагменте кода элементу zip из address присваивается значение 93456. worker.address.zip = 93456; В каждой структуре любой член обозначают с помощью тех структур, в которые он вложен — начиная от самых общих и заканчивая той, непосредственно в которой он находится. В соответствии со стандартом С89 структуры могут быть вложенными вплоть до 15-го уровня. А стандарт С99 допускает уровень вложенности до 63-го включительно. Объединения Объединение — это место в памяти, которое используется для хранения переменных, разных типов. Объединение дает возможность интерпретировать один и тот же набор битов не менее, чем двумя разными способами. Объявление объединения (начинается с ключевого слова union) похоже на объявление структуры и в общем виде выглядит так: union тег { тип имя-члена; тип имя-члена; тип имя-члена; . . . } переменные-этого-объединения; Например: union u_type { int i; char ch; }; Это объявление не создает никаких переменных. Чтобы объявить переменную, ее имя можно поместить в конце объявления или написать отдельный оператор объявления. Чтобы с помощью только что написанного кода объявить переменную-объединение, которая называется cnvt и имеет тип u_type, можно написать следующий оператор: union u_type cnvt; В cnvt одну и ту же область памяти занимают целая переменная i и символьная переменная ch. Конечно, i занимает 2 байта (при условии, что целые значения занимают по 2 байта), a ch — только 1. В любом месте программы хранящиеся в cnvt данные можно обрабатывать как целые или символьные. Как i, так и ch, хранятся в объединении cnvt (подразумевается, что целые значения занимают по 2 байта) |<------ i ------>| | | +--------+--------+ | Байт 0 | Байт 1 | +--------+--------+ | | |<- ch -> Когда переменная объявляется с ключевым словом union, компилятор автоматически выделяет столько памяти, чтобы в ней поместился самый большой член нового объединения. Например, при условии, что целые значения занимают по 2 байта, для размещения i в cnvt необходимо, чтобы длина этого объединения составляла 2 байта, даже если для ch требуется только 1 байт. Для получения доступа к члену объединения используйте тот же синтаксис, что и для структур: операторы точки и стрелки. При работе непосредственно с объединением следует пользоваться точкой. А при получении доступа к объединению с помощью указателя нужен оператор стрелка. Например, чтобы присвоить целое значение 10 элементу i из cnvt, напишите cnvt.i = 10; В следующем примере функции func1 передается указатель на cnvt: void func1(union u_type *un) { un->i = 10; /* присвоение cnvt значение 10 с помощью указателя */ } Объединения часто используются тогда, когда нужно выполнить специфическое преобразование типов, потому что хранящиеся в объединениях данные можно обозначать совершенно разными способами. Например, используя объединения, можно манипулировать байтами, составляющими значение типа double, и делать так, чтобы менять его точность или выполнять какое-либо необычное округление. Чтобы получить представление о полезности объединений в случаях, когда нужны нестандартные преобразования типа, подумайте над проблемой записи целых значений типа short в файл, который находится на диске. В стандартной библиотеке языка С не определено никакой функции, специально предназначенной для выполнения этой записи. Хотя данные любого типа можно записывать в файл, пользуясь функцией fwrite(), но было бы нерационально применять этот способ для такой простой операции, как запись на диск целых значений типа short, так как получится чрезмерный перерасход ресурсов. А вот, используя объединение, можно легко создать функцию putw(), которая по одному байту будет записывать в файл двоичное представление целого значения типа short. (В этом примере предполагается, что такие значения имеют длину 2 байта каждое.) Создадим объединение, состоящее из целой переменной типа short и из массива 2-байтовых символов: union pw { short int i; char ch[2]; }; Теперь с помощью pw можно написать вариант putw(), приведенный в следующей программе. #include #include union pw { short int i; char ch[2]; }; int putw(short int num, FILE *fp); int main(void) { FILE *fp; fp = fopen("test.tmp", "wb+"); if(fp == NULL) { printf("Файл не открыт.\n"); exit(1); } putw(1025, fp); /* запись значения 1025 */ fclose(fp); return 0; } int putw(short int num, FILE *fp) { union pw word; word.i = num; putc(word.ch[0], fp); /* записать первую половину */ return putc(word.ch[1], fp); /* записать вторую половину */ } Хотя функция putw() и вызывается с целым аргументом типа short, ей для выполнения побайтовой записи в файл на диске все равно приходится использовать стандартную функцию putc(). Битовые поля В отличие от некоторых других компьютерных языков, в языке С имеется встроенная поддержка битовых полей, которая дает возможность получать доступ к единичному биту. Битовые поля могут быть полезны по разным причинам, а именно: • Если память ограничена, то в одном байте можно хранить несколько булевых переменных (принимающих значения ИСТИНА и ЛОЖЬ); • Некоторые устройства передают информацию о состоянии, закодированную в байте в одном или нескольких битах; • Для некоторых процедур шифрования требуется доступ к отдельным битам внутри байта. Хотя для решения этих задач можно успешно применять побитовые операции, битовые поля могут придать вашему коду больше упорядоченности (и, возможно, с их помощью удастся достичь большей эффективности). Битовое поле может быть членом структуры или объединения. Оно определяет длину поля в битах. Общий вид определения битового поля такой: тип имя : длина; Здесь тип означает тип битового поля, а длина — количество бит, которые занимает это поле. Тип битового поля может быть int, signed или unsigned. (Кроме того, в соответствии со стандартом С99, у битового поля еще может быть тип _Вооl.) Битовые поля часто используются при анализе данных, поступающих в программу с аппаратуры. Например, в результате опроса состояния адаптера последовательной связи может возвращаться байт состояния, организованный следующим образом: Бит Что означает, если установлен Изменение в линии сигнала разрешения на передачу (change in clear-to-send line) 1 Изменение состояния готовности устройства сопряжения (change in data-set-ready) 2 Обнаружена концевая запись (trailing edge detected) 3 Изменение в приемной линии (change in receive line) 4 Разрешение на передачу. Сигналом CTS (clear-to-send) модем разрешает подключенному терминалу передавать данные 5 Модем готов (data-set-ready) 6 Телефонный вызов (telephone ringing) 7 Сигнал принят (received signal) Информацию в байте состояния можно представить с помощью следующего битового поля: struct status_type { unsigned delta_cts: 1; unsigned delta_dsr: 1; unsigned tr_edge: 1; unsigned delta_rec: 1; unsigned cts: 1; unsigned dsr: 1; unsigned ring: 1; unsigned rec_line: 1; } status; Для того чтобы программа могла определить, когда можно отправлять или принимать данные, можно использовать такие операторы: status = get_port_status(); if(status.cts) printf("Разрешение на передачу"); if(status.dsr) printf("Данные готовы"); Для присвоения битовому полю значения используйте тот же способ, что и для элемента, находящегося в структуре любого другого типа. Вот, например, фрагмент кода, выполняющий сброс поля ring: status.ring = 0; Каждое битовое поле доступно с помощью оператора точка. Однако если структура передана с помощью указателя, то следует использовать оператор стрелка ->. Нет необходимости давать имя каждому битовому полю. Таким образом можно легко получать доступ к нужному биту, обходя неиспользуемые. Например, если вас интересуют только биты cts и dsr, то структуру status_type можно объявить таким образом: struct status_type { unsigned : 4; unsigned cts: 1; unsigned dsr: 1; } status; Кроме того, если биты, расположенные после dsr, не используются, то определять их не надо. В структурах можно сочетать обычные члены с битовыми полями. Например, в структуре struct emp { struct addr address; float pay; unsigned lay_off: 1; /* временно уволенный или работающий */ unsigned hourly: 1; /* почасовая оплата или оклад */ unsigned deductions: 3; /* налоговые (IRS) удержания */ }; определены данные о работнике, для которых выделяется только один байт, содержащий информацию трех видов: статус работника, на окладе ли он, а также количество удержаний из его зарплаты. Без битового поля эта информация занимала бы 3 байта. Использование битовых полей имеет определенные ограничения. Нельзя получить адрес битового поля. Нет массивов битовых данных. При переносе кода на другую машину неизвестно, будут ли поля обрабатываться справа налево или слева направо; это значит, что выполнение любого кода, в котором используются битовые поля, в определенной степени может зависеть от машины, на которой он выполняется. Другие ограничения будут зависеть от конкретных реализаций. Перечисления Перечисление — это набор именованных целых констант. Перечисления довольно часто встречаются в повседневной жизни. Вот, например, перечисление, в котором приведены названия монет, используемых в Соединенных Штатах: penny (пенни, монета в один цент), nickel (никель, монета в пять центов), dime (монета в 10 центов), quarter (25 центов, четверть доллара), half-dollar (полдоллара), dollar (доллар) Перечисления определяются во многом так же, как и структуры; началом объявления перечислимого типа служит ключевое слово enum. Перечисление в общем виде выглядит так: enum тег {список перечисления} список переменных; Здесь тег и список переменных не являются обязательными. (Но хотя бы что-то одно из них должно присутствовать.) Следующий фрагмент кода определяет перечисление с именем coin (монета): enum coin { penny, nickel, dime, quarter, half_dollar, dollar}; Тег перечисления можно использовать для объявления переменных данного перечислимого типа. Вот код, в котором money (деньги) объявляется в качестве переменной типа coin: enum coin money; С учетом этих объявлений совершенно верными являются следующие операторы: money = dime; if(money==quarter) printf("Денег всего четверть доллара.\n"); Главное, что нужно знать для понимания перечислений — каждый их элемент представляет целое число. В таком виде элементы перечислений можно применять везде, где используются целые числа. Каждому элементу дается значение, на единицу большее, чем у его предшественника. Первый элемент перечисления имеет значение 0. Поэтому, при выполнении кода printf("%d %d", penny, dime); на экран будет выведено 0 2. Однако для одного или более элементов можно указать значение, используемое как инициализатор. Для этого после перечислителя надо поставить знак равенства, а затем — целое значение. Перечислителям, которые идут после инициализатора, присваиваются значения, большие предшествующего. Например, следующий код присваивает quarter значение 100: enum coin { penny, nickel, dime, quarter=100, half_dollar, dollar}; вот какие значения появились у этих элементов: penny 0 nickel 1 dime 2 quarter 100 half_dollar 101 dollar 102 Относительно перечислений есть одно распространенное, но ошибочное мнение. Оно состоит в том, что их элементы можно непосредственно вводить и выводить. Это не так. Например, следующий фрагмент кода не будет выполняться так, как того ожидают многие неопытные программисты: /* этот код работать не будет */ money = dollar; printf("%s", money); Здесь dollar — это имя для значения целого типа; это не строка. Таким образом, попытка вывести money в виде строки по существу обречена. По той же причине для достижения нужных результатов не годится и такой код: /* этот код не правильный */ strcpy(money, "dime"); То есть строка, содержащая имя элемента, автоматически в этот перечислитель не превратится. На самом же деле создавать код для ввода и вывода элементов перечислений — это довольно-таки скучное занятие (но его можно избежать лишь тогда, когда будет достаточно именно целых значений этих перечислителей). Например, чтобы выводить название монеты, вид которой находится в money, потребуется следующий код: switch(money) { case penny: printf("пенни"); break; case nickel: printf("никель"); break; case dime: printf("монета в 10 центов"); break; case quarter: printf("четверть доллара"); break; case half_dollar: printf("полдоллара"); break; case dollar: printf("доллар"); } Иногда можно объявить строчный массив и использовать значение перечисления как индекс при переводе этого значения в соответствующую строку. Например, следующий код также выводит нужную строку: char name[][12]={ "пенни", "никель", "монета в 10 центов", "четверть доллара", "полдоллара", "доллар" }; printf("%s", name[money]); Конечно, он будет работать только тогда, когда не инициализирован ни один из элементов перечисления, так как строчный массив должен иметь индекс, который начинается с 0 и возрастает каждый раз на 1. Так как при операциях ввода/вывода необходимо специально заботиться о преобразовании перечислений в их строчный эквивалент, который можно легко прочитать, то перечисления полезнее всего именно в тех процедурах, где такие преобразования не нужны. Например, перечисления часто применяются, чтобы определить таблицы соответствия символов в компиляторах. Важное различие между С и С++ Что касается имен типов структур, объединений и перечислений, то между языками С и C++ имеется важное различие. Чтобы понять эту разницу, проанализируйте следующее объявление структуры: struct MyStruct { int a; int b; } ; В языке С имя MyStruct называется тегом. Чтобы объявить объект типа MyStruct, необходимо использовать выражение, аналогичное следующему: struct MyStruct obj; Перед именем MyStruct находится ключевое слово struct. Однако в C++ для этой операции достаточно использовать объявление покороче: MyStruct obj; /* Нормально для C++, неправильно для C */ В C++ не требуется ключевое слово struct. В C++, как только структура объявлена, можно объявлять переменные ее типа, используя только ее тег и не ставя перед ним ключевого слова struct. Дело здесь в том, что в С имя структуры не определяет полное имя типа. Вот почему в С это имя называется не полным именем, а тегом. Однако в C++ имя структуры является полным именем типа, и оно может использоваться для определения переменных. Не надо, впрочем, забывать, что до сих пор можно на вполне законных основаниях в программе на языке C++ использовать и объявление в стиле С. Это можно обобщить для объединений и перечислений. Таким образом, в С при объявлении объектов непосредственно перед тегом имени должно находиться одно из ключевых слов: struct, union или enum (в зависимости от конкретного случая). А в C++ ключевое слово не требуется. Так как для C++ подходят объявления в стиле С, то во время переноса программ из С в C++ по этому поводу беспокоиться нечего. Но при переносе из C++ в С соответствующие изменения сделать придется. Использование sizof для обеспечения переносимости Вы имели возможность убедиться, что структуры и объединения можно использовать для создания переменных разных размеров, а также в том, что настоящий размер этих переменных в разных машинах может быть разным. Оператор sizeof подсчитывает размер любой переменной или любого типа и может быть полезен, если в программах требуется свести к минимуму машинно-зависимый код. Этот оператор особенно полезен там, где приходится иметь дело со структурами или объединениями. Предположим, что определенные типы данных имеют следующие размеры: Тип Размер в байтах char 1 int 4 double 8 Поэтому при выполнении следующего кода на экран будут выведены числа 1, 4 и 8: char ch; int i; double f; printf("%d", sizeof(ch)); printf("%d", sizeof(i)); printf("%d", sizeof(f)); Размер структуры равен сумме размеров ее членов или, возможно, даже больше этой суммы. Рассмотрим пример: struct s { char ch; int i; double f; } s_var; Здесь sizeof(s_var) равняется как минимум 13 (=8+4+1). Однако размер s_var может быть и больше, потому что компилятору иногда необходимо специально увеличить размер структуры, выровнять некоторые ее члены на границу слова или параграфа. (Параграф занимает 16 байтов.) Так как размер структуры может быть больше, чем сумма размеров ее членов, то всегда, когда нужно знать размер структуры, следует использовать sizeof. Например, если требуется динамически выделять память для объекта типа s, необходимо использовать последовательность операторов, аналогичную той, что показана здесь (а не вставлять вручную значения длины его членов): struct s *p; p = malloc(sizeof(struct s)); Так как sizeof — это оператор времени компиляции, то вся информация, необходимая для вычисления размера любой переменной, становится известной как раз во время компиляции. Это особенно важно для объединений, потому что размер каждого из них всегда равен размеру наибольшего члена. Например, проанализируйте следующее объединение: union u { char ch; int i; double f; } u_var; Для него sizeof(u_var) равняется 8. Впрочем, во время выполнения не имеет значения, какой размер на самом деле имеет u_var. Важен размер его наибольшего члена, так как любое объединение должно быть такого же размера, как и его самый большой элемент. Средство typedef Новые имена типов данных можно определять, используя ключевое слово typedef. На самом деле таким способом новый тип данных не создается, а всего лишь определяется новое имя для уже существующего типа. Этот процесс может помочь сделать машинно-зависимые программы более переносимыми. Если вы для каждого машинно-зависимого типа данных, используемого в вашей программе, определяете данное вами имя, то при компиляции для новой среды придется менять только операторы typedef. Такие выражения могут помочь в самодокументировании кода, позволяя давать понятные имена стандартным типам данных. Общий вид декларации typedef (оператора typedef) такой: typedef тип новое_имя; где тип — это любой тип данных языка С, а новое_имя — новое имя этого типа. Новое имя является дополнением к уже существующему, а не его заменой. Например, для float можно создать новое имя с помощью typedef float balance; Это выражение дает компилятору указание считать balance еще одним именем float. Затем, используя balance, можно создать переменную типа float: balance over_due; Теперь имеется переменная с плавающей точкой over_due типа balance, a balance является еще одним именем типа float. Теперь, когда имя balance определено, его можно использовать и в другом операторе typedef. Например, выражение typedef balance overdraft; дает компилятору указание признавать overdraft в качестве еще одного имени balance, которое в свою очередь является еще одним именем float. Использование операторов typedef может облегчить чтение кода и его перенос на новую машину. Однако новый физический тип данных таким способом вы не создадите. 26 Организация линейных списков: линейный однонаправленный односвязный список Линейный список - это конечная последовательность однотипных элементов (узлов), возможно, с повторениями. Количество элементов в последовательности называется длиной списка, причем длина в процессе работы программы может изменяться. Линейный список F, состоящий из элементов D1,D2,...,Dn, записывают в виде последовательности значений заключенной в угловые скобки F=, или представляют графически D1  D2  D3  ...  Dn линейный список Например, F1=<2,3,1>,F2=<7,7,7,2,1,12>, F3=<>. Длина списков F1, F2, F3 равна соответственно 3,6,0. При работе со списками на практике чаще всего приходится выполнять следующие операции: - найти элемент с заданным свойством; - определить первый элемент в линейном списке; - вставить дополнительный элемент до или после указанного узла; - исключить определенный элемент из списка; - упорядочить узлы линейного списка в определенном порядке. В реальных языках программирования нет какой-либо структуры данных для представления линейного списка так, чтобы все указанные операции над ним выполнялись в одинаковой степени эффективно. Поэтому при работе с линейными списками важным является представление используемых в программе линейных списков таким образом, чтобы была обеспечена максимальная эффективность и по времени выполнения программы, и по объему требуемой памяти. Методы хранения линейных списков разделяются на методы последовательного и связанного хранения. Рассмотрим простейшие варианты этих методов для списка с целыми значениями F=<7,10>. При последовательном хранении элементы линейного списка размещаются в массиве d фиксированных размеров, например, 100, и длина списка указывается в переменной l, т.е. в программе необходимо иметь объявления вида float d[100]; int l; Размер массива 100 ограничивает максимальные размеры линейного списка. Список F в массиве d формируется так: d[0]=7; d[1]=10; l=2; Полученный список хранится в памяти согласно схеме. l: 2 d: 7 10 ...   [0] [1] [2] [3]   [98] [99] Последовательное хранение линейного списка. При связанном хранении в качестве элементов хранения используются структуры, связанные по одной из компонент в цепочку, на начало которой (первую структуру) указывает указатель dl. Структура образующая элемент хранения, должна кроме соответствующего элемента списка содержать и указатель на соседний элемент хранения. Описание структуры и указателя в этом случае может имееть вид: typedef struct snd /* структура элемента хранения */ { float val; /* элемент списка */ struct snd *n ; /* указатель на элемент хранения */ } DL; DL *p; /* указатель текущего элемента */ DL *dl; /* указатель на начало списка */ Для выделения памяти под элементы хранения необходимо пользоваться функцией malloc(sizeof(DL)) или calloc(l,sizeof(DL)). Формирование списка в связанном хранении может осуществляется операторами: p=malloc(sizeof(DL)); p->val=10; p->n=NULL; dl=malloc(sizeof(DL)); dl->val=7; dl->n=p; В последнем элементе хранения (конец списка) указатель на соседний элемент имеет значение NULL. Операции со списками при последовательном хранении При выборе метода хранения линейного списка следует учитывать, какие операции будут выполняться и с какой частотой, время их выполнения и объем памяти, требуемый для хранения списка. Пусть имеется линейный список с целыми значениями и для его хранения используется массив d (с числом элементов 100), а количество элементов в списке указывается переменной l. Реализация операций над списком представляется следующими фрагментами программ которые используют объявления: float d[100]; int i,j,l; 1) печать значения первого элемента (узла) if (i<0 || i>l) printf("\n нет элемента"); else printf("d[%d]=%f ",i,d[i]); 2) удаление элемента, следующего за i-тым узлом if (i>=l) printf("\n нет следующего "); l--; for (j=i+1;j<="1" ||="" i="">=l) printf("\n нет соседа"); else printf("\n %d %d",d[i-1],d[i+1]); 4) добавление нового элемента new за i-тым узлом if (i==l || i>l) printf("\n нельзя добавить"); else { for (j=l; j>i+1; j--) d[j+1]=d[j]; d[i+1]=new; l++; } 5) частичное упорядочение списка с элементами К1,К2,...,Кl в список K1',K2',...,Ks,K1,Kt",...,Kt", s+t+1=l так, чтобы K1'=K1; после упорядочения указатель v указывает ND *v; float k1; k1=dl->val; r=dl; while( r->n!=NULL ) { v=r->n; if (v->valn=v->n; v->n=dl; dl=v; } else r=v; } Количество действий, требуемых для выполнения указанных операций над списком в связанном хранении, оценивается соотношениями: для операций 1 и 2 - Q=l; для операций 3 и 4 - Q=1; для операции 5 - Q=l. 27 Битовые поля структур и объединения Битовые поля Особую разновидность структур представляют собой битовые поля. Битовое поле - это последовательность соседних битов внутри одного, целого значения. Оно может иметь тип signed int или unsigned int и занимать от 1 до 16 битов. Поля размещаются в машинном слове в направлении от младших к старшим разрядам. Например, структура: struct prim { int a:2; unsigned b:3; int c:5; int d:1; unsigned d:5; } i, j; обеспечивает размещение данных в двух байтах (в одном слове). Если бы последнее поле было задано так: unsigned d:6, то оно размещалось бы не в первом слове, а в разрядах 0 - 5 второго слова. В полях типа signed крайний левый бит является знаковым. Поля используются для упаковки значений нескольких переменных в одно машинное слово с целью экономии памяти. Они не могут быть массивами и не имеют адресов, поэтому к ним нельзя применять унарную операцию &. Объединение (union) Объединение - это некоторая переменная, которая может хранить (в разное время) объекты различного типа и размера. В результате появляется возможность работы в одной и той же области памяти с данными различного вида. Для описания объединения используется ключевое слово union, а соответствующий синтаксис аналогичен структурам. Пусть задано определение: union r {int ir; float fr; char cr;} z; Здесь ir имеет размер 2 байта, fr - 4 байта, cr - 1 байт. Размер переменной z будет равен размеру самого большого из трех приведенных типов (т.е. 4 байтам). В один и тот же момент времени z может иметь значение только одной из переменных ir, fr или cr. Перечислимый тип данных Перечислимый тип данных предназначен для описания объектов из некоторого заданного множества. Он задается ключевым словом enum. Рассморим пример: enum seasons (spring, summer, autumn, winter); Здесь введен новый тип данных seasons. Теперь можно определить переменные этого типа: enum seasons а, b, с; Каждая из них (а, b, c) может принимать одно из четырех значений: spring, summer, autumn и winter. Эти переменные можно было определить сразу при описании типа: enum seasons (spring, summer, autumn, winter) a, b, с; Рассмотрим еще один пример: enum days {mon, tues, wed, thur, fri, sat, sun} my_week; Имена, занесенные в days (также как и в seasons в предыдущем примере), представляют собой константы целого типа. Первая из них (mon) автоматически устанавливается в нуль, и каждая следующая имеет значение на единицу больше, чем предыдущая (tues=1, wed=2 и т.д.). Можно присвоить константам определенные значения целого типа (именам, не имеющим их, будут, как и раньше, назначены значения предыдущих констант, увеличенные на единицу). Например: enum days (man=5, tues=8, wed=10, thur, fri, sat, sun} my_week; После этого mon=5, tues=8,wed=10, thur=11, fri=12, sat=13, sun=14. Тип enum можно использовать для задания констант true=1 и false=0, например: enum t_f (false, true) а, b; 28 Потоковый ввод-вывод. Типы потоков, основные функции работы с потоками Файловый ввод / вывод в С и С++ Так как С является фундаментом C++, то иногда возникает путаница в отношениях его файловой системы с аналогичной системой C++. Во-первых, C++ поддерживает всю файловую систему С. Таким образом, при перемещении более старого С-кода в C++ нет необходимости менять все процедуры ввода/вывода. Во-вторых, следует иметь в виду, что в C++ определена своя собственная, объектно-ориентированная система ввода/вывода, в которую входят как функции, так и операторы ввода/вывода. В системе ввода/вывода C++ полностью поддерживаются все возможности аналогичной системы С и это делает излишней файловую систему языка С. Вообще говоря, при написании программ на языке C++ обычно более удобно использовать именно его систему ввода/вывода, но, если необходимо воспользоваться файловой системой языка С, то это также вполне возможно. Файловый ввод / вывод в стандартном С и UNIX Первоначально язык С был реализован в операционной системе UNIX. Как таковые, ранние версии С (да и многие нынешние) поддерживают набор функций ввода/вывода, совместимый с UNIX. Этот набор иногда называют UNIX-подобной системой ввода/вывода или небуферизованной системой ввода/вывода. Однако когда С был стандартизован, то UNIX-подобные функции в него не вошли — в основном из-за того, что оказались лишними. Кроме того, UNIX-подобная система может оказаться неподходящей для некоторых сред, которые могут поддерживать язык С, но не эту систему ввода/вывода. Потоки и файлы В системе ввода/вывода С для программ поддерживается единый интерфейс, не зависящий от того, к какому конкретному устройству осуществляется доступ. То есть в этой системе между программой и устройством находится нечто более общее, чем само устройство. Такое обобщенное устройство ввода или вывода (устройство более высокого уровня абстракции) называется потоком, в то время как конкретное устройство называется файлом. (Впрочем, файл — тоже понятие абстрактное.) Потоки Файловая система языка С предназначена для работы с самыми разными устройствами, в том числе терминалами, дисководами и накопителями на магнитной ленте. Даже если какое-то устройство сильно отличается от других, буферизованная файловая система все равно представит его в виде логического устройства, которое называется потоком. Все потоки ведут себя похожим образом. И так как они в основном не зависят от физических устройств, то та же функция, которая выполняет запись в дисковый файл, может ту же операцию выполнять и на другом устройстве, например, на консоли. Потоки бывают двух видов: текстовые и двоичные. Текстовые потоки Текстовый поток — это последовательность символов. В стандарте С считается, что текстовый поток организован в виде строк, каждая из которых заканчивается символом новой строки. Однако в конце последней строки этот символ не является обязательным. В текстовом потоке по требованию базовой среды могут происходить определенные преобразования символов. Например, символ новой строки может быть заменен парой символов — возврата каретки и перевода строки. Поэтому может и не быть однозначного соответствия между символами, которые пишутся (читаются), и теми, которые хранятся во внешнем устройстве. Кроме того, количество тех символов, которые пишутся (читаются), и тех, которые хранятся во внешнем устройстве, может также не совпадать из-за возможных преобразований. Двоичные потоки Двоичный поток — это последовательность байтов, которая взаимно однозначно соответствует байтам на внешнем устройстве, причем никакого преобразования символов не происходит. Кроме того, количество тех байтов, которые пишутся (читаются), и тех, которые хранятся на внешнем устройстве, одинаково. Однако в конце двоичного потока может добавляться определяемое приложением количество нулевых байтов. Такие нулевые байты, например, могут использоваться для заполнения свободного места в блоке памяти незначащей информацией, чтобы она в точности заполнила сектор на диске. Файлы В языке С файлом может быть все что угодно, начиная с дискового файла и заканчивая терминалом или принтером. Поток связывают с определенным файлом, выполняя операцию открытия. Как только файл открыт, можно проводить обмен информацией между ним и программой. Но не у всех файлов одинаковые возможности. Например, к дисковому файлу прямой доступ возможен, в то время как к некоторым принтерам — нет. Таким образом, мы пришли к одному важному принципу, относящемуся к системе ввода/вывода языка С: все потоки одинаковы, а файлы — нет. Если файл может поддерживать запросы на местоположение (указатель текущей позиции), то при открытии такого файла указатель текущей позиции в файле устанавливается в начало. При чтении из файла (или записи в него) каждого символа указатель текущей позиции увеличивается, обеспечивая тем самым продвижение по файлу. Файл отсоединяется от определенного потока (т.е. разрывается связь между файлом и потоком) с помощью операции закрытия. При закрытии файла, открытого с целью вывода, содержимое (если оно есть) связанного с ним потока записывается на внешнее устройство. Этот процесс, который обычно называют дозаписью потока, гарантирует, что никакая информация случайно не останется в буфере диска. Если программа завершает работу нормально, т.е. либо main() возвращает управление операционной системе, либо вызывается exit(), то все файлы закрываются автоматически. В случае аварийного завершения работы программы, например, в случае краха или завершения путем вызова abort(), файлы не закрываются. У каждого потока, связанного с файлом, имеется управляющая структура, содержащая информацию о файле; она имеет тип FILE. В этом блоке управления файлом[2] никогда ничего не меняйте[3]. Если вы новичок в программировании, то разграничение потоков и файлов может показаться излишним или даже "заумным". Однако надо помнить, что основная цель такого разграничения — это обеспечить единый интерфейс. Для выполнения всех операций ввода/вывода следует использовать только понятия потоков и применять всего лишь одну файловую систему. Ввод или вывод от каждого устройства автоматически преобразуется системой ввода/вывода в легко управляемый поток. Основы файловой системы Файловая система языка С состоит из нескольких взаимосвязанных функций. Самые распространенные из них (для их работы требуется заголовок ): Имя Что делает fopen() Открывает файл fclose() Закрывает файл putc() Записывает символ в файл fputc() To же, что и putc() getc() Читает символ из файла fgetc() To же, что и getc() fgets() Читает строку из файла fputs() Записывает строку в файл fseek() Устанавливает указатель текущей позиции на определенный байт файла ftell() Возвращает текущее значение указателя текущей позиции в файле fprintf() Для файла то же, что printf() для консоли fscanf() Для файла то же, что scanf() для консоли feof() Возвращает значение true (истина), если достигнут конец файла ferror() Возвращает значение true, если произошла ошибка rewind() Устанавливает указатель текущей позиции в начало файла remove() Стирает файл fflush() Дозапись потока в файл Заголовок предоставляет прототипы функций ввода/вывода и определяет следующие три типа: size_t, fpos_t и FILE. size_t и fpos_t представляют собой определенные разновидности такого типа, как целое без знака. Кроме того, в определяется несколько макросов: NULL, EOF, FOPEN_MAX, SEEK_SET, SEEK_CUR и SEEK_END. Макрос NULL определяет пустой (null) указатель. Макрос EOF, часто определяемый как -1, является значением, возвращаемым тогда, когда функция ввода пытается выполнить чтение после конца файла. FOPEN_MAX определяет целое значение, равное максимальному числу одновременно открытых файлов. Другие макросы используются вместе с fseek() — функцией, выполняющей операции прямого доступа к файлу. Указатель файла Указатель файла — это то, что соединяет в единое целое всю систему ввода/вывода языка С. Указатель файла — это указатель на структуру типа FILE. Он указывает на структуру, содержащую различные сведения о файле, например, его имя, статус и указатель текущей позиции в начало файла. В сущности, указатель файла определяет конкретный файл и используется соответствующим потоком при выполнении функций ввода/вывода. Чтобы выполнять в файлах операции чтения и записи, программы должны использовать указатели соответствующих файлов. Чтобы объявить переменную-указатель файла, используйте такого рода оператор: FILE *fp; Открытие файла Функция fopen() открывает поток и связывает с этим потоком определенный файл. Затем она возвращает указатель этого файла. Чаще всего под файлом подразумевается дисковый файл. Прототип функции fopen() такой: FILE *fopen(const char *имя_файла, const char *режим); где имя_файла — это указатель на строку символов, представляющую собой допустимое имя файла, в которое также может входить спецификация пути к этому файлу. Строка, на которую указывает режим, определяет, каким образом файл будет открыт. Строки, подобные "r+b" могут быть представлены и в виде "rb+". Допустимые значения режим Режим Что означает r Открыть текстовый файл для чтения w Создать текстовый файл для записи a Добавить в конец текстового файла rb Открыть двоичный файл для чтения wb Создать двоичный файл для записи ab Добавить в конец двоичного файла r+ Открыть текстовый файл для чтения/записи w+ Создать текстовый файл для чтения/записи a+ Добавить в конец текстового файла или создать текстовый файл для чтения/записи r+b Открыть двоичный файл для чтения/записи w+b Создать двоичный файл для чтения/записи a+b Добавить в конец двоичного файла или создать двоичный файл для чтения/записи Функция fopen() возвращает указатель файла. Никогда не следует изменять значение этого указателя в программе. Если при открытии файла происходит ошибка, то fopen() возвращает пустой (null) указатель. В следующем коде функция fopen() используется для открытия файла по имени TEST для записи. FILE *fp; fp = fopen("test", "w"); Хотя предыдущий код технически правильный, но его обычно пишут немного по-другому: FILE *fp; if ((fp = fopen("test","w"))==NULL) { printf("Ошибка при открытии файла.\n"); exit(1); } Этот метод помогает при открытии файла обнаружить любую ошибку, например, защиту от записи или полный диск, причем обнаружить еще до того, как программа попытается в этот файл что-либо записать. Вообще говоря, всегда нужно вначале получить подтверждение, что функция - fopen() выполнилась успешно, и лишь затем выполнять с файлом другие операции. Хотя название большинства файловых режимов объясняет их смысл, однако не помешает сделать некоторые дополнения. Если попытаться открыть файл только для чтения, а он не существует, то работа fopen() завершится отказом. А если попытаться открыть файл в режиме дозаписи, а сам этот файл не существует, то он просто будет создан. Более того, если файл открыт в режиме дозаписи, то все новые данные, которые записываются в него, будут добавляться в конец файла. Содержимое, которое хранилось в нем до открытия (если только оно было), изменено не будет. Далее, если файл открывают для записи, но выясняется, что он не существует, то он будет создан. А если он существует, то содержимое, которое хранилось в нем до открытия, будет утеряно, причем будет создан новый файл. Разница между режимами r+ и w+ состоит в том, что если файл не существует, то в режиме открытия r+ он создан не будет, а в режиме w+ все произойдет наоборот: файл будет создан! Более того, если файл уже существует, то открытие его в режиме w+ приведет к утрате его содержимого, а в режиме r+ оно останется нетронутым. Из табл. 9.2 видно, что файл можно открыть либо в одном из текстовых, либо в одном из двоичных режимов. В большинстве реализаций в текстовых режимах каждая комбинация кодов возврата каретки (ASCII 13) и конца строки (ASCII 10) преобразуется при вводе в символ новой строки. При выводе же происходит обратный процесс: символы новой строки преобразуются в комбинацию кодов возврата каретки (ASCII 13) и конца строки (ASCII 10). В двоичных режимах такие преобразования не выполняются. Максимальное число одновременно открытых файлов определяется FOPEN_MAX. Это значение не меньше 8, но чему оно точно равняется — это должно быть написано в документации по компилятору. Закрытие файла Функция fclose() закрывает поток, который был открыт с помощью вызова fopen().Функция fclose() записывает в файл все данные, которые еще оставались в дисковом буфере, и проводит, так сказать, официальное закрытие файла на уровне операционной системы. Отказ при закрытии потока влечет всевозможные неприятности, включая потерю данных, испорченные файлы и возможные периодические ошибки в программе. Функция fclose() также освобождает блок управления файлом, связанный с этим потоком, давая возможность использовать этот блок снова. Так как количество одновременно открытых файлов ограничено, то, возможно, придется закрывать один файл, прежде чем открывать другой. Прототип функции fclose() такой: int fclose(FILE *уф); где уф — указатель файла, возвращенный в результате вызова fopen(). Возвращение нуля означает успешную операцию закрытия. В случае же ошибки возвращается EOF. Чтобы точно узнать, в чем причина этой ошибки, можно использовать стандартную функцию ferror() (о которой вскоре пойдет речь). Обычно отказ при выполнении fclose() происходит только тогда, когда диск был преждевременно удален (стерт) с дисковода или на диске не осталось свободного места. Запись символа В системе ввода/вывода языка С определяются две эквивалентные функции, предназначенные для вывода символов: putc() и fputc(). (На самом деле putc() обычно реализуется в виде макроса.) Две идентичные функции имеются просто потому, чтобы сохранять совместимость со старыми версиями С. Мы будем использовать putc(), но применение fputc() также вполне возможно. Функция putc() записывает символы в файл, который с помощью fopen() уже открыт в режиме записи. Прототип этой функции следующий: int putc(int ch, FILE *уф); где уф — это указатель файла, возвращенный функцией fopen(), a ch — выводимый символ. Указатель файла сообщает putc(), в какой именно файл следует записывать символ. Хотя ch и определяется как int, однако записывается только младший байт. Если функция putc() выполнилась успешно, то возвращается записанный символ. В противном же случае возвращается EOF. Чтение символа Для ввода символа также имеются две эквивалентные функции: getc() и fgetc(). Обе определяются для сохранения совместимости со старыми версиями С. Функция getc() записывает символы в файл, который с помощью fopen() уже открыт в режиме для чтения. Прототип этой функции следующий: int getc(FILE *уф); где уф — это указатель файла, имеющий тип FILE и возвращенный функцией fopen(). Функция getc() возвращает целое значение, но символ находится в младшем байте. Если не произошла ошибка, то старший байт (байты) будет обнулен. Если достигнут конец файла, то функция getc() возвращает EOF. Поэтому, чтобы прочитать символы до конца текстового файла, можно использовать следующий код; do { ch = getc(fp); } while(ch!=EOF); Однако getc() возвращает EOF и в случае ошибки. Для определения того, что же на самом деле произошло, можно использовать ferror(). Использование fopen(), getc(), putc(), и fclose() Функции fopen(), getc(), putc() и fclose() — это минимальный набор функций для операций с файлами. Следующая программа, KTOD, представляет собой простой пример, в котором используются только функции putc(), fopen() и fclose(). В этой программе символы считываются с клавиатуры и записываются в дисковый файл до тех пор, пока пользователь не введет знак доллара. Имя файла определяется в командной строке. Например, если вызвать программу KTOD, введя в командной строке KTOD TEST, то строки текста будут вводиться в файл TEST. /* KTOD: программа ввода с клавиатуры на диск. */ #include #include int main(int argc, char *argv[]) { FILE *fp; char ch; if(argc!=2) { printf("Вы забыли ввести имя файла.\n"); exit(1); } if((fp=fopen(argv[1], "w"))==NULL) { printf("Ошибка при открытии файла.\n"); exit(1); } do { ch = getchar(); putc(ch, fp); } while (ch != '$'); fclose(fp); return 0; } Программа DTOS, являющаяся дополнением к программе KTOD, читает любой текстовый файл и выводит его содержимое на экран. /* DTOS: программа, которая читает файлы и выводит их на экран. */ #include #include int main(int argc, char *argv[]) { FILE *fp; char ch; if(argc!=2) { printf("Вы забыли ввести имя файла.\n"); exit(1); } if((fp=fopen(argv[1], "r"))==NULL) { printf("Ошибка при открытии файла.\n"); exit(1); } ch = getc(fp); /* чтение одного символа */ while (ch!=EOF) { putchar(ch); /* вывод на экран */ ch = getc(fp); } fclose(fp); return 0; } Испытывая эти две программы, вначале с помошью KTOD создайте текстовый файл, а затем с помошью DTOS прочитайте его содержимое. Использование feof() Если достигнут конец файла, то getc() возвращает EOF. Однако проверка значения, возвращенного getc(), возможно, не является наилучшим способом узнать, достигнут ли конец файла. Во-первых, файловая система языка С может работать как с текстовыми, так и с двоичными файлами. Когда файл открывается для двоичного ввода, то может быть прочитано целое значение, которое, как выяснится при проверке, равняется EOF. В таком случае программа ввода сообщит о том, что достигнут конец файла, чего на самом деле может и не быть. Во-вторых, функция getc() возвращает EOF и в случае отказа, а не только тогда, когда достигнут конец файла. Если использовать только возвращаемое значение getc(), то невозможно определить, что же на самом деле произошло. Для решения этой проблемы в С имеется функция feof(), которая определяет, достигнут ли конец файла. Прототип функции feof() такой: int feof(FILE *уф); Если достигнут конец файла, то feof() возвращает true (истина); в противном же случае эта функция возвращает нуль. Поэтому следующий код будет читать двоичный файл до тех пор, пока не будет достигнут конец файла: while(!feof(fp)) ch = getc(fp); Ясно, что этот метод можно применять как к двоичным, так и к текстовым файлам. В следующей программе, которая копирует текстовые или двоичные файлы, имеется пример применения feof(). Файлы открываются в двоичном режиме, а затем feof() проверяет, не достигнут ли конец файла. /* Копирование файла. */ #include #include int main(int argc, char *argv[]) { FILE *in, *out; char ch; if(argc!=3) { printf("Вы забыли ввести имя файла.\n"); exit(1); } if((in=fopen(argv[1], "rb"))==NULL) { printf("Нельзя открыть исходный файл.\n"); exit(1); } if((out=fopen(argv[2], "wb")) == NULL) { printf("Нельзя открыть файл результатов.\n"); exit(1); } /* Именно этот код копирует файл. */ while(!feof(in)) { ch = getc(in); if(!feof(in)) putc(ch, out); } fclose(in); fclose(out); return 0; } Ввод / вывод строк: fputs() и fgets() Кроме getc() и putc(), в языке С также поддерживаются родственные им функции fgets() и fputs(). Первая из них читает строки символов из файла на диске, а вторая записывает строки такого же типа в файл, тоже находящийся на диске. Эти функции работают почти как putc() и getc(), но читают и записывают не один символ, а целую строку. Прототипы функций fgets() и fputs() следующие: int fputs(const char *cmp, FILE *уф); char *fgets(char *cmp, int длина, FILE *уф); Функция fputs() пишет в определенный поток строку, на которую указывает cmp. В случае ошибки эта функция возвращает EOF. Функция fgets() читает из определенного потока строку, и делает это до тех пор, пока не будет прочитан символ новой строки или количество прочитанных символов не станет равным длина-1. Если был прочитан разделитель строк, он записывается в строку, чем функция fgets() отличается от функции gets(). Полученная в результате строка будет оканчиваться символом конца строки ('0'). При успешном завершении работы функция возвращает cmp, а в случае ошибки — пустой указатель (null). В следующей программе показано использование функции fputs(). Она читает строки с клавиатуры и записывает их в файл, который называется TEST. Чтобы завершить выполнение программы, введите пустую строку. Так как функция gets() не записывает разделитель строк, то его приходится специально вставлять перед каждой строкой, записываемой в файл; это делается для того, чтобы файл было легче читать: #include #include #include int main(void) { char str[80]; FILE *fp; if((fp = fopen("TEST", "w"))==NULL) { printf("Ошибка при открытии файла.\n"); exit(1); } do { printf("Введите строку (пустую - для выхода):\n"); gets(str); strcat(str, "\n"); /* добавление разделителя строк */ fputs(str, fp); } while(*str!='\n'); return 0; } Функция rewind() Функция rewind() устанавливает указатель текущей позиции в файле на начало файла, указанного в качестве аргумента этой функции. Иными словами, функция rewind() выполняет "перемотку" (rewind) файла. Вот ее прототип: void rewind(FILE *уф); где уф — это допустимый указатель файла. Изменим программу таким образом, чтобы она отображала содержимое файла сразу после его создания. Чтобы выполнить отображение, программа после завершения ввода "перематывает" файл, а затем с помощью fback() читает его с самого начала. Сейчас файл необходимо открыть в режиме чтения/записи, используя в качестве аргумента, задающего режим, строку "w+". #include #include #include int main(void) { char str[80]; FILE *fp; if((fp = fopen("TEST", "w+"))==NULL) { printf("Ошибка при открытии файла.\n"); exit(1); } do { printf("Введите строку (пустую - для выхода):\n"); gets(str); strcat(str, "\n"); /* ввод разделителя строк */ fputs(str, fp); } while(*str!='\n'); /* теперь выполняется чтение и отображение файла */ rewind(fp); /* установить указатель текущей позиции на начало файла. */ while(!feof(fp)) { fgets(str, 79, fp); printf(str); } return 0; } Функция ferror() Функция ferror() определяет, произошла ли ошибка во время выполнения операции с файлом. Прототип этой функции следующий: int ferror(FILE *уф); где уф — допустимый указатель файла. Она возвращает значение true (истина), если при последней операции с файлом произошла ошибка; в противном же случае она возвращает false (ложь). Так как при любой операции с файлом устанавливается свое условие ошибки, то после каждой такой операции следует сразу вызывать ferror(), а иначе данные об ошибке могут быть потеряны. В следующей программе показано применение ferror(). Программа удаляет табуляции из файла, заменяя их соответствующим количеством пробелов. Размер табуляции определяется макросом TAB_SIZE. ferror() вызывается после каждой операции с файлом. При запуске этой программы указывайте в командной строке имена входного и выходного файлов. /* Программа заменяет в текстовом файле символы табуляции пробелами и отслеживает ошибки. */ #include #include #define TAB_SIZE 8 #define IN 0 #define OUT 1 void err(int e); int main(int argc, char *argv[]) { FILE *in, *out; int tab, i; char ch; if(argc!=3) { printf("Синтаксис: detab <входной_файл> <выходной файл>\n"); exit(1); } if((in = fopen(argv[1], "rb"))==NULL) { printf("Нельзя открыть %s.\n", argv[1]); exit(1); } if((out = fopen(argv[2], "wb"))==NULL) { printf("Нельзя открыть %s.\n", argv[2]); exit(1); } tab = 0; do { ch = getc(in); if(ferror(in)) err(IN); /* если найдена табуляция, выводится соответствующее число пробелов */ if(ch=='\t') { for(i=tab; i<8; i++) { putc(' ', out); if(ferror(out)) err(OUT); } tab = 0; } else { putc(ch, out); if(ferror(out)) err(OUT); tab++; if(tab==TAB_SIZE) tab = 0; if(ch=='\n' || ch=='\r') tab = 0; } } while(!feof(in)); fclose(in); fclose(out); return 0; } void err(int e) { if(e==IN) printf("Ошибка при вводе.\n"); else printf("Ошибка привыводе.\n"); exit(1); } Стирание файлов Функция remove() стирает указанный файл. Вот ее прототип: int remove(const char *имя_файла); В случае успешного выполнения эта функция возвращает нуль, а в противном случае — ненулевое значение. Следующая программа стирает файл, указанный в командной строке. Однако вначале она дает возможность передумать. Утилита, подобная этой, может пригодиться компьютерным пользователям-новичкам. /* Двойная проверка перед стиранием. */ #include #include #include int main(int argc, char *argv[]) { char str[80]; if(argc!=2) { printf("Синтаксис: xerase <имя_файла>\n"); exit(1); } printf("Стереть %s? (Y/N): ", argv[1]); gets(str); if(toupper(*str)=='Y') if(remove(argv[1])) { printf("Нельзя стиреть файл.\n"); exit(1); } return 0; } Дозапись потока Для дозаписи содержимого выводного потока в файл применяется функция fflush(). Вот ее прототип: int fflush(FILE *уф); Эта функция записывает все данные, находящиеся в буфере в файл, который указан с помощью уф. При вызове функции fflush() с пустым (null) указателем файла уф будет выполнена дозапись во все файлы, открытые для вывода. После своего успешного выполнения fflush() возвращает нуль, в противном случае — EOF. Функции fread() и fwrite() Для чтения и записи данных, тип которых может занимать более 1 байта, в файловой системе языка С имеется две функции: fread() и fwrite(). Эти функции позволяют читать и записывать блоки данных любого типа. Их прототипы следующие: size_t fread(void *буфер, size_t колич_байт, size_t счетчик, FILE *уф); size_t fwrite(const void *буфер, size_t колич_байт, size_t счетчик, FILE *уф); Для fread() буфер — это указатель на область памяти, в которую будут прочитаны данные из файла. А для fwrite() буфер — это указатель на данные, которые будут записаны в файл. Значение счетчик определяет, сколько считывается или записывается элементов данных, причем длина каждого элемента в байтах равна колич_байт. (Вспомните, что тип size_t определяется как одна из разновидностей целого типа без знака.) И, наконец, уф — это указатель файла, то есть на уже открытый поток. Функция fread() возвращает количество прочитанных элементов. Если достигнут конец файла или произошла ошибка, то возвращаемое значение может быть меньше, чем счетчик. А функция fwrite() возвращает количество записанных элементов. Если ошибка не произошла, то возвращаемый результат будет равен значению счетчик. Использование fread() и fwrite() Как только файл открыт для работы с двоичными данными, fread() и fwrite() соответственно могут читать и записывать информацию любого типа. Например, следующая программа записывает в дисковый файл данные типов double, int и long, a затем читает эти данные из того же файла. /* Запись несимвольных данных в дисковый файл и последующее их чтение. */ #include #include int main(void) { FILE *fp; double d = 12.23; int i = 101; long l = 123023L; if((fp=fopen("test", "wb+"))==NULL) { printf("Ошибка при открытии файла.\n"); exit(1); } fwrite(&d, sizeof(double), 1, fp); fwrite(&i, sizeof(int), 1, fp); fwrite(&l, sizeof(long), 1, fp); rewind(fp); fread(&d, sizeof(double), 1, fp); fread(&i, sizeof(int), 1, fp); fread(&l, sizeof(long), 1, fp); printf("%f %d %ld", d, i, l); fclose(fp); return 0; } Как видно из этой программы, в качестве буфера можно использовать (и часто именно так и делают) просто память, в которой размещена переменная. В этой простой программе значения, возвращаемые функциями fread() и fwrite(), игнорируются. Однако на практике эти значения необходимо проверять, чтобы обнаружить ошибки. Одним из самых полезных применений функций fread() и fwrite() является чтение и запись данных пользовательских типов, особенно структур. Например, если определена структура struct struct_type { float balance; char name[80]; } cust; то следующий оператор записывает содержимое cust в файл, на который указывает fp: fwrite(&cust, sizeof(struct struct_type), 1, fp); Пример со списком рассылки Чтобы показать, как можно легко записывать большие объемы данных, пользуясь функциями fread() и fwrite(), мы переделаем программу работы со списком рассылки. Усовершенствованная версия сможет сохранять адреса в файле. Как и раньше, адреса будут храниться в массиве структур следующего типа: struct addr { char name[30]; char street[40]; char city[20]; char state[3]; unsigned long int zip; } addr_list[MAX]; Значение MAX определяет максимальное количество адресов, которое может быть в списке. При выполнении программы поле name каждой структуры инициализируется пустым указателем (NULL). В программе свободной считается та структура, поле name которой содержит строку нулевой длины, т.е. имя адресата представляет собой пустую строку. Далее приведены функции save() и load(), которые используются соответственно для сохранения и загрузки базы данных (списка рассылки). Обратите внимание, насколько кратко удалось закодировать каждую из функций, а ведь эта краткость достигнута благодаря мощи fread() и fwrite()! И еше обратите внимание на то, как эти функции проверяют значения, возвращаемые функциями fread() и fwrite(), чтобы обнаружить таким образом возможные ошибки. /* Сохранение списка. */ void save(void) { FILE *fp; register int i; if((fp=fopen("maillist", "wb"))==NULL) { printf("Ошибка при открытии файла.\n"); return; } for(i=0; i #include #define MAX 100 struct addr { char name[30]; char street[40]; char city[20]; char state[3]; unsigned long int zip; } addr_list[MAX]; void init_list(void), enter(void); void delete(void), list(void); void load(void), save(void); int menu_select(void), find_free(void); int main(void) { char choice; init_list(); /* инициализация массива структур */ for(;;) { choice = menu_select(); switch(choice) { case 1: enter(); break; case 2: delete(); break; case 3: list(); break; case 4: save(); break; case 5: load(); break; case 6: exit(0); } } return 0; } /* Инициализация списка. */ void init_list(void) { register int t; for(t=0; t6); return c; } /* Добавление адреса в список. */ void enter(void) { int slot; char s[80]; slot = find_free(); if(slot==-1) { printf("\nСписок заполнен"); return; } printf("Введите имя: "); gets(addr_list[slot].name); printf("Введите улицу: "); gets(addr_list[slot].street); printf("Введите город: "); gets(addr_list[slot].city); printf("Введите штат: "); gets(addr_list[slot].state); printf("Введите почтовый индекс: "); gets(s); addr_list[slot].zip = strtoul(s, '\0', 10); } /* Поиск свободной структуры. */ int find_free(void) { register int t; for(t=0; addr_list[t].name[0] && t=0 && slot < MAX) addr_list[slot].name[0] = '\0'; } /* Вывод списка на экран. */ void list(void) { register int t; for(t=0; t #include int main(int argc, char *argv[]) { FILE *fp; if(argc!=3) { printf("Синтаксис: SEEK <имя_файла> <байт>\n"); exit(1); } if((fp = fopen(argv[1], "rb"))==NULL) { printf("Ошибка при открытии файла.\n"); exit(1); } if(fseek(fp, atol(argv[2]), SEEK_SET)) { printf("Seek error.\n"); exit(1); } printf("В %ld-м байте содержится %c.\n", atol(argv[2]), getc(fp)); fclose(fp); return 0; } Функцию fseek() можно использовать для доступа внутри многих значений одного типа, просто умножая размер данных на номер элемента, который вам нужен. Например, предположим, имеется список рассылки, который состоит из структур типа addr (определенных ранее). Чтобы получить доступ к десятому адресу в файле, в котором хранятся адреса, используйте следующий оператор: fseek(fp, 9*sizeof(struct addr), SEEK_SET); Текущее значение указателя текущей позиции в файле можно определить с помощью функции ftell(). Вот ее прототип: long int ftell(FILE *уф); Функция возвращает текущее значение указателя текущей позиции в файле, связанном с указателем файла уф. При неудачном исходе она возвращает -1. Обычно прямой доступ может потребоваться лишь для двоичных файлов. Причина тут простая — так как в текстовых файлах могут выполняться преобразования символов, то может и не быть прямого соответствия между тем, что находится в файле и тем байтом, к которому нужен доступ. Единственный случай, когда надо использовать fseek() для текстового файла — это доступ к той позиции, которая была уже найдена с помощью ftell(); такой доступ выполняется с помощью макроса SEEK_SET, используемого в качестве начала отсчета. Хорошо помните следующее: даже если в файле находится один только текст, все равно этот файл при необходимости можно открыть и в двоичном режиме. Никакие ограничения, связанные с тем, что файлы содержат текст, к операциям прямого доступа не относятся. Эти ограничения относятся только к файлам, открытым в текстовом режиме. Функции fprinf() и fscanf() Кроме основных функций ввода/вывода, о которых шла речь, в системе ввода/вывода языка С также имеются функции fprintf() и fscanf(). Эти две функции, за исключением того, что предназначены для работы с файлами, ведут себя точно так же, как и printf() и scanf(). Прототипы функций fprintf() и fscanf() следующие: int fprintf(FILE *уф, const char *управляющая_строка, ...); int fscanf(FILE *уф, const char *управляющая_строка, ...); где уф — указатель файла, возвращаемый в результате вызова fopen(). Операции ввода/вывода функции fprintf() и fscanf() выполняют с тем файлом, на который указывает уф. В качестве примера предлагается рассмотреть следующую программу, которая читает с клавиатуры строку и целое значение, а затем записывает их в файл на диске; имя этого файла — TEST. После этого программа читает этот файл и выводит информацию на экран. После запуска программы проверьте, каким получится файл TEST. /* пример использования fscanf() и fprintf() */ #include #include #include int main(void) { FILE *fp; char s[80]; int t; if((fp=fopen("test", "w")) == NULL) { printf("Ошибка открытия файла.\n"); exit(1); } printf("Введите строку и число: "); fscanf(stdin, "%s%d", s, &t); /* читать с клавиатуры */ fprintf(fp, "%s %d", s, t); /* писать в файл */ fclose(fp); if((fp=fopen("test","r")) == NULL) { printf("Ошибка при открытии файла.\n"); exit(1); } fscanf(fp, "%s%d", s, &t); /* чтение из файла */ fprintf(stdout, "%s %d", s, t); /* вывод на экран */ return 0; } Маленькое предупреждение. Хотя читать разносортные данные из файлов на дисках и писать их в файлы, расположенные также на дисках, часто легче всего именно с помошью функций fprintf() и fscanf(), но это не всегда самый эффективный способ выполнения операций чтения и записи. Так как данные в формате ASCII записываются так, как они должны появиться на экране (а не в двоичном виде), то каждый вызов этих функций сопряжен с определенными накладными расходами. Поэтому, если надо заботиться о размере файла или скорости, то, скорее всего, придется использовать fread() и fwrite(). Стандартные потоки Что касается файловой системы языка С, то в начале выполнения программы автоматически открываются три потока. Это stdin (стандартный поток ввода), stdout (стандартный поток вывода) и stderr (стандартный поток ошибок). Обычно эти потоки направляются к консоли, но в средах, которые поддерживают перенаправление ввода/вывода, они могут быть перенаправлены операционной системой на другое устройство. (Перенаправление ввода/вывода поддерживается, например, такими операционными системами, как Windows, DOS, UNIX и OS/2.) Так как стандартные потоки являются указателями файлов, то они могут использоваться системой ввода/вывода языка С также для выполнения операций ввода/вывода на консоль. Например, putchar() может быть определена таким образом: int putchar(char c) { return putc(c, stdout); } Вообще говоря, stdin используется для считывания с консоли, a stdout и stderr — для записи на консоль. В роли указателей файлов потоки stdin, stdout и stderr можно применять в любой функции, где используется переменная типа FILE *. Например, для ввода строки с консоли можно написать примерно такой вызов fgets(): char str[255]; fgets(str, 80, stdin); И действительно, такое применение fgets() может оказаться достаточно полезным. При использовании gets() не исключена возможность, что массив, который используется для приема вводимых пользователем символов, будет переполнен. Это возможно потому, что gets() не проводит проверку на отсутствие нарушения границ. Полезной альтернативой gets() является функция fgets() с аргументом stdin, так как эта функция может ограничивать число читаемых символов и таким образом не допустить переполнения массива. Единственная проблема, связанная с fgets(), состоит в том, что она не удаляет символ новой строки (в то время как gets() удаляет!), поэтому его приходится удалять "вручную", как показано в следующей программе: #include #include int main(void) { char str[80]; int i; printf("Введите строку: "); fgets(str, 10, stdin); /* удалить символ новой строки, если он есть */ i = strlen(str)-1; if(str[i]=='\n') str[i] = '\0'; printf("Это Ваша строка: %s", str); return 0; } He забывайте, что stdin, stdout и stderr — это не переменные в обычном смысле, и им нельзя присваивать значение с помощью fopen(). Кроме того, именно потому, что в начале работы программы эти указатели файлов создаются автоматически, в конце работы они и закрываются автоматически. Так что и не пытайтесь самостоятельно их закрыть. Связь с консольным вводом / выводом В языке С консольный и файловый ввод/вывод не слишком отличаются друг от друга. Функции консольного ввода/вывода на самом деле направляют результаты своих операций на один из потоков — stdin или stdout, и по сути, каждая из них является специальной версией соответствующей файловой функции. Функции консольного ввода/вывода для того и существуют, чтобы было удобно именно программисту. Ввод/вывод на консоль можно выполнять с помощью любой файловой функции языка С. Однако для вас может быть сюрпризом, что, оказывается, операции ввода/вывода на дисковых файлах можно выполнять с помощью функции консольного ввода/вывода, например, printf()! Дело в том, что все функции консольного ввода/вывода выполняют свои операции с потоками stdin и stdout. В средах, поддерживающих перенаправление ввода/вывода, это равносильно тому, что stdin или stdout могут быть перенаправлены на устройство, отличное от клавиатуры или экрана. Проанализируйте, например, следующую программу: #include int main(void) { char str[80]; printf("Введите строку: "); gets(str); printf(str); return 0; } Предположим, что эта программа называется TEST. При ее нормальном выполнении на экран выводится подсказка, затем читается строка, введенная с клавиатуры, и, наконец, эта строка выводится на экран. Однако в средах, в которых поддерживается перенаправление ввода/вывода, один из потоков stdin или stdout (или оба одновременно) можно перенаправить в файл. Например, в среде DOS или Windows следующий запуск TEST TEST > OUTPUT приводит к тому, что вывод этой программы будет записан в файл по имени OUTPUT. А следующий запуск TEST TEST < INPUT > OUTPUT направляет поток stdin в файл по имени INPUT, а поток стандартного вывода — в файл по имени OUTPUT. Когда С-программа завершается, то все перенаправленные потоки возвращаются в состояния, которые были установлены по умолчанию. Перенаправление стандартных потоков: функция freopen() Для перенаправления стандартных потоков можно воспользоваться функцией freopen(). Эта функция связывает имеющийся поток с новым файлом. Так что она вполне может связать с новым файлом и стандартный поток. Вот прототип этой функции: FILE *freopen(const char *имя_файла, const char *режим, FILE *поток); где имя_файла — это указатель на имя файла, который требуется связать с потоком, на который указывает указатель поток. Файл открывается в режиме режим; этот параметр может принимать те же значения, что и соответствующий параметр функции fopen(). Если функция freopen() выполнилась успешно, то она возвращает поток, а если встретились ошибки, — то NULL. В следующей программе показано использование функции freopen() для перенаправления стандартного потока вывода stdout в файл с именем OUTPUT. #include int main(void) { char str[80]; freopen("OUTPUT", "w", stdout); printf("Введите строку: "); gets(str); printf(str); return 0; } Вообще говоря, перенаправление стандартных потоков с помощью freopen() в некоторых случаях может быть полезно, например, при отладке. Однако выполнение дисковых операций ввода/вывода на перенаправленных потоках stdin и stdout не настолько эффективно, как использование таких функций, как fread() или fwrite(). 29 Препроцессор языка Си: директивы, макросы и предопределенные макросы Имеются следующие директивы препроцессора: #define #endif #ifdef #line #elif #error #ifndef #pragma #else #if #include #undef Все они начинаются со знака #. Кроме того, каждая директива препроцессора должна занимать отдельную строку. Например, строка #include #include рассматривается как недопустимая. Директива #define Директива #define определяет идентификатор и последовательность символов, которая будет подставляться вместо идентификатора каждый раз, когда он встретится в исходном файле. Идентификатор называется именем макроса, а сам процесс замены — макрозаменой(макрорасширением, макрогенерацией и макроподстановкой. Определение макроса часто называют макроопределением, а обращение к макросу — макровызовом или макрокомандой; иногда макроопределение также называется макрокомандой). В общем виде директива выглядит таким образом: #define имя_макроса последовательность_символов В этом выражении нет точки с запятой. Между идентификатором и последовательностью символов последовательность_символов может быть любое количество пробелов, но признаком конца последовательности символов может быть только разделитель строк. Предположим, например, что вместо значения 1 нужно использовать слово LEFT (левый), а вместо значения 0 — слово RIGHT (правый). Тогда можно сделать следующие объявления с помощью директивы #define: #define LEFT 1 #define RIGHT 0 В результате компилятор будет подставлять 1 или 0 каждый раз, когда в вашем файле исходного кода встречается идентификатор соответственно LEFT или RIGHT. Например, следующий код выводит на экран 0 1 2: printf("%d %d %d", RIGHT, LEFT, LEFT+1); После определения имя макроса можно использовать в определениях других имен макросов. Вот, например, код, определяющий значения ONE (один), TWO (два) и three (три): #define ONE 1 #define TWO ONE+ONE #define THREE ONE+TWO Макроподстановка — это просто замена какого-либо идентификатора связанной с ним последовательностью символов. Поэтому если требуется определить стандартное сообщение об ошибке, то можно написать примерно следующее: #define E_MS "стандартная ошибка при вводе\n" /* ... */ printf(E_MS); Теперь каждый раз, когда встретится идентификатор E_MS, компилятор будет его заменять строкой "стандартная ошибка при вводе\n". Для компилятора выражение printf() на самом деле будет выглядеть таким образом: printf("стандартная ошибка при вводе\n"); Если идентификатор находится внутри строки, заключенной в кавычки, то замены не будет. Например, при выполнении кода #define XYZ это проверка printf("XYZ"); вместо сообщения это проверка будет выводиться последовательность символов XYZ. Если последовательность_символов не помещается в одной строке, то эту последовательность можно продолжить на следующей строке, поместив в конце предыдущей, как показано ниже, обратную косую черту: #define LONG_STRING "это очень длинная \ строка, используемая в качестве примера" Программисты, пишущие программы на языке С, в именах определяемых идентификаторов часто используют буквы верхнего регистра. Если разработчики программ следуют этому правилу, то тот, кто будет читать их программу, с первого взгляда поймет, что будет происходить макрозамена. Кроме того, все директивы #define обычно лучше всего помещать в самом начале файла или в отдельном заголовочном файле, а не разбрасывать по всей программе. Имена макросов часто используются для определения имен так называемых "магических чисел" (встречающихся в программе). Например, имеется программа, в которой определяется массив и несколько процедур, получающих доступ к этому массиву. Вместо того чтобы размер массива "зашивать в код" в виде константы, этот размер можно определить с помощью оператора #define, а затем использовать это имя макроса везде, где требуется размер массива. Таким образом, если требуется изменить этот размер, то потребуется изменить только соответствующий оператор #define, a затем перекомпилировать программу. Рассмотрим, например, фрагмент программы #define MAX_SIZE 100 /* ... */ float balance[MAX_SIZE]; /* ... */ for(i=0; i #define ABS(a) (a) < 0 ? -(a) : (a) int main(void) { printf("модули чисел -1 и 1 равны соответственно %d и %d", ABS(-1), ABS(1)); return 0; } Во время компиляции этой программы вместо формального параметра а из определения макроса будут подставляться значения -1 и 1. Скобки, в которых находится а, позволяют в любом случае сделать правильную замену. Например, если скобки, стоящие вокруг а, удалить, то выражение ABS(10-20) после макрозамены будет преобразовано в 10-20 < 0 ? -10-20 : 10-20 и может привести к неправильному результату. Использование вместо настоящих функций макросов с формальными параметрами дает одно существенное преимущество: увеличивается скорость выполнения кода, потому что в таких случаях не надо тратить ресурсы на вызов функций. Однако если у макроса с формальными параметрами очень большие размеры, то тогда из-за дублирования кода увеличение скорости достигается за счет увеличения размеров программы. И вот еще что: хотя макросы с формальными параметрами являются полезным средством, но в С99 (и в C++) есть еще более эффективный способ создания машинной программы — с использованием ключевого слово inline. В С99 можно определить макрос с переменным количеством формальных параметров. Директива #error Директива #error заставляет компилятор прекратить компиляцию. Эта директива используется в основном для отладки. В общем виде директива #error выглядит таким образом: #еrrоr сообщение-об-ошибке сообщение-об-ошибке в двойные кавычки не заключается. Когда встречается директива #error, то выводится сообщение об ошибке — возможно, вместе с другой информацией, определяемой компилятором. Директива #include Директива #include дает указание компилятору читать еще один исходный файл — в дополнение к тому файлу, в котором находится сама эта директива. Имя исходного файла должно быть заключено в двойные кавычки или в угловые скобки. Например, обе директивы #include "stdio.h" #include дают компилятору указание читать и компилировать заголовок для библиотечных функций системы ввода/вывода. Файлы, имена которых находятся в директивах #include, могут в свою очередь содержать другие директивы #include. Они называются вложенными директивами #include. Количество допустимых уровней вложенности у разных компиляторов может быть разным. Однако в стандарте С89 предусмотрено, что компиляторы должны допускать не менее 8 таких уровней. А в стандарте С99 предусмотрена поддержка не менее 15 уровней вложенности. Способ поиска файла зависит от того, заключено ли его имя в двойные кавычки или же в угловые скобки. Если имя заключено в угловые скобки, то поиск файла проводится тем способом, который определен в компиляторе. Часто это означает поиск определенного каталога, специально предназначенного для хранения таких файлов. Если имя заключено в кавычки, то поиск файла проводится другим способом. Во многих компиляторах это означает поиск файла в текущем рабочем каталоге. Если же файл не найден, то поиск повторяется уже так, как будто имя файла заключено в угловые скобки. Обычно большинство программистов имена стандартных заголовочных файлов заключают в угловые скобки. А использование кавычек обычно приберегается для имен специальных файлов, относящихся к конкретной программе. Впрочем, твердого и простого правила, по которому кавычки требуется использовать именно таким образом, не существует. В С-программе директиву #include можно использовать не только для указания имени файла, содержащего обычный исходный текст программы, но и для указания заголовка. В языке С определен набор стандартных заголовков, содержащих необходимую информацию о различных библиотеках этого языка. Заголовок — это стандартный идентификатор, который может соответствовать имени файла, а может и не соответствовать ему. Таким образом, заголовок является просто абстракцией, которая гарантирует наличие некоторой информации. Однако на практике в языке С заголовки почти всегда являются именами файлов. Директивы условной компиляции Имеется несколько директив, которые дают возможность выборочно компилировать части исходного кода вашей программы. Этот процесс называется условной компиляцией и широко используется фирмами, живущими за счет коммерческого программного обеспечения — теми, которые поставляют и поддерживают многие специальные версии одной программы. Директивы #if, #else, #elif и #endif Возможно, самыми распространенными директивами условной компиляции являются #if, #else, #elif и #endif. Они дают возможность в зависимости от значения константного выражения включать или исключать те или иные части кода. В общем виде директива #if выглядит таким образом: #if константное выражение последовательность операторов #endif Если находящееся за #if константное выражение истинно, то компилируется код, который находится между этим выражением и #endif. В противном случае этот промежуточный код пропускается. Директива #endif обозначает конец блока #if. Например, /* Простой пример #if. */ #include #define MAX 100 int main(void) { #if MAX>99 printf("Компилирует для массива, размер которого больше 99.\n"); #endif return 0; } Это программа выводит сообщение на экран, потому что МАХ больше 99. В этом примере показано нечто очень важное. Значение выражения, находящегося за директивой #if, должно быть вычислено во время компиляции. Поэтому в этом выражении могут находиться только ранее определенные идентификаторы и константы, — но не переменные. Директива #else работает в основном так, как else — ключевое слово языка С: задает альтернативу на тот случай, если не выполнено условие #if. Предыдущий пример можно дополнить следующим образом: /* Простой пример #if/#else. */ #include #define MAX 10 int main(void) { #if MAX>99 printf("Компилирует для массива, размер которого больше 99.\n"); #else printf("Компилирует для небольшого массива.\n"); #endif return 0; } В этом случае выясняется, что МАХ меньше 99, поэтому часть кода, относящаяся к #if, не компилируется. Однако компилируется альтернативный код, относящийся к #else, и откомпилированная программа будет отображать сообщение Компилируется для небольшого массива. Директива #else используется для того, чтобы обозначить и конец блока #if, и начало блока #else. Это естественно, поскольку любой директиве #if может соответствовать только одна директива #endif. Директива #elif означает "else if" и устанавливает для множества вариантов компиляции цепочку if-else-if. После #elif находится константное выражение. Если это выражение истинно, то компилируется находящийся за ним блок кода, и больше не проверяются никакие другие выражения #elif. В противном же случае проверяется следующий блок этой последовательности. В общем виде #elif выглядит таким образом: #if выражение последовательность операторов #elif выражение 1 последовательность операторов #elif выражение 2 последовательность операторов #elif выражение 3 последовательность операторов #elif выражение 4 . . . #elif выражение N последовательность операторов #endif Например, в следующем фрагменте для определения знака денежной единицы используется значение ACTIVE_COUNTRY (для какой страны): #define US 0 #define ENGLAND 1 #define FRANCE 2 #define ACTIVE_COUNTRY US #if ACTIVE_COUNTRY == US char currency[] = "dollar"; #elif ACTIVE_COUNTRY == ENGLAND char currency[] = "pound"; #else char currency[] = "franc"; #endif В соответствии со стандартом С89 у директив #if и #elif может быть не менее 8 уровней вложенности. А в соответствии со стандартом С99 программистам разрешается использовать не менее 63 уровней вложенности. При вложенности каждая директива #endif, #else или #elif относится к ближайшей директиве #if или #elif. Например, совершенно правильным является следующий фрагмент кода: #if MAX>100 #if SERIAL_VERSION int port=198; #elif int port=200; #endif #else char out_buffer[100]; #endif Директивы #ifdef и #ifndef Другой способ условной компиляции — это использование директив #ifdef и #ifndef, которые соответственно означают "if defined" (если определено) и "if not defined" (если не определено). В общем виде #ifdef выглядит таким образом: #ifdef имя_макроса последовательность операторов #endif Блок кода будет компилироваться, если имя макроса было определено ранее в операторе #define. В общем виде оператор #ifndef выглядит таким образом: #ifndef имя_макроса последовательность операторов #endif Блок кода будет компилироваться, если имя макроса еще не определено в операторе #define. И в #ifdef, и в #ifndef можно использовать оператор #else или #elif. Например, #include #define TED 10 int main(void) { #ifdef TED printf("Привет, Тед\n"); #else printf("Привет, кто-нибудь\n"); #endif #ifndef RALPH printf("А RALPH не определен, т.ч. Ральфу не повезло.\n"); #endif return 0; } выведет Привет, Тед, а также A RALPH не определен, т.ч. Ральфу не повезло. В соответствии со стандартом С89 допускается не менее 8 уровней #ifdef и #ifndef. А стандарт С99 устанавливает, что должно поддерживаться не менее 63 уровней вложенности. Директива #undef Директива #undef удаляет ранее заданное определение имени макроса, то есть "аннулирует" его определение; само имя макроса должно находиться после директивы. В общем виде директива #undef выглядит таким образом: #undef имя_макроса Вот как, например, можно использовать эту директиву: #define LEN 100 #define WIDTH 100 char array[LEN][WIDTH]; #undef LEN #undef WIDTH /* а здесь и LEN и WIDTH уже не определены */ И LEN, и WIDTH определены, пока не встретился оператор #undef. Директива #undef используется в основном для того, чтобы локализовать имена макросов в тех участках кода, где они нужны. Использование defined Кроме применения #ifdef, есть еще второй способ узнать, определено ли имя макроса. Можно использовать директиву #if в сочетании с оператором времени компиляции defined. В общем виде оператор defined выглядит таким образом: defined имя_макроса Если имя_макроса определено, то выражение считается истинным; в противном случае — ложным. Например, чтобы узнать, определено ли имя макроса MYFILE, можно использовать одну из двух команд препроцессора: #if defined MYFILE или #ifdef MYFILE Можно также задать противоположное условие, поставив ! прямо перед defined. Например, следующий фрагмент компилируется только тогда, когда имя макроса DEBUG не определено: #if !defined DEBUG printf("Окончательная версия!\n"); #endif Единственная причина, по которой используется оператор defined, состоит в том, что с его помощью в #elif можно узнать, определено ли имя макроса. Директива #line Директива #line изменяет содержимое __LINE__ и __FILE__, которые являются зарезервированными идентификаторами в компиляторе. В первом из них содержится номер компилируемой в данный момент строки кода. А второй идентификатор — это строка, содержащая имя компилируемого исходного файла. В общем виде директива #line выглядит таким образом: #line номер "имя_файла" где номер — это положительное целое число, которое становится новым значением __LINE__, а необязательное имя_файла — это любой допустимый идентификатор файла, становящийся новым значением __FILE__. Директива #line в основном используется для отладки и специальных применений. Например, следующий код определяет, что счетчик строк будет начинаться с 100, а оператор printf() выводит номер 102, потому что он расположен в третьей строке программы после оператора #line 100: #include #line 100 /* установить счетчик строк */ int main(void) /* строка 100 */ { /* строка 101 */ printf("%d\n",__LINE__); /* строка 102 */ return 0; } Директива #pragma Директива #pragma — это определяемая реализацией директива, которая позволяет передавать компилятору различные инструкции. Например, компилятор может поддерживать трассировку выполнения программы. Тогда возможность трассировки можно указывать в операторе #pragma. Возможности этой директивы и относящиеся к ней подробности должны быть описаны в документации по компилятору. В стандарте С99 директиве #pragma есть альтернатива — оператор _Pragma. Операторы препроцессора # и ## Имеется два оператора препроцессора: # и ##. Они применяются в сочетании с оператором #define. Оператор #, который обычно называют оператором превращения в строку (stringize), превращает аргумент, перед которым стоит, в строку, заключенную в кавычки. Рассмотрим, например, следующую программу: #include #define mkstr(s) # s int main(void) { printf(mkstr(Мне нравится C)); return 0; } Препроцессор превращает строку printf(mkstr(Мне нравится C)); в printf("Мне нравится C"); Оператор ##, который называют оператором склеивания (pasting), или конкатенации конкатенирует две лексемы. Рассмотрим, например, программу #include #define concat(a, b) a ## b int main(void) { int xy = 10; printf("%d", concat(x, y)); return 0; } Препроцессор преобразует printf("%d", concat(x, y)); в printf("%d", xy); Если эти операторы покажутся вам незнакомыми, то надо помнить вот о чем: они не являются необходимыми и не используются в большинстве программ. В общем-то, эти операторы предусмотрены для работы препроцессора в некоторых особых случаях. Имена предопределенных макрокоманд В языке С определены пять встроенных, предопределенных имен макрокоманд. Вот они: __LINE__ __FILE__ __DATE__ __TIME__ __STDC__ В такой же последовательности о них здесь и пойдет речь. Об именах макросов __LINE__ и __FILE__ рассказывалось, когда говорилось о директиве #line. Говоря кратко, они содержат соответственно номер строки и имя файла компилируемой программы. В имени макроса __DATE__ содержится строка в виде месяц/день/год, то есть дата перевода исходного кода в объектный. В имени макроса __TIME__ содержится время компиляции программы. Это время представлено строкой, имеющей вид час:минута:cекунда. Если __STDC__ определено как 1, то тогда компилятор выполняет компиляцию в соответствии со стандартом С. А что касается С99, то в этом стандарт определены еще два имени макросов: __STDC_HOSTED__ __STDC_VERSION__ __STDC_HOSTED__ равняется 1 для тех сред, в которых выполнение происходит под управлением операционной системы, и 0 — в противном случае. __STDC_VERSION__ будет равно как минимум 199901 и будет увеличиваться с каждой новой версией языка С.
«Алгоритмические языки и программирование» 👇
Готовые курсовые работы и рефераты
Купить от 250 ₽
Решение задач от ИИ за 2 минуты
Решить задачу
Помощь с рефератом от нейросети
Написать ИИ
Получи помощь с рефератом от ИИ-шки
ИИ ответит за 2 минуты

Тебе могут подойти лекции

Смотреть все 588 лекций
Все самое важное и интересное в Telegram

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

Перейти в Telegram Bot