Будь умным!


У вас вопросы?
У нас ответы:) SamZan.ru

Искусственный интеллект Программное обеспечение информационных технологий Автоматизированные си

Работа добавлена на сайт samzan.ru: 2015-07-05


Диплом на заказ

М. П. Батура, В. Л. Бусько, А. Г. Корбит, Т. М. Кривоносова

Основы алгоритмизации и программирования.

Язык Си

Допущено Министерством образования Республики Беларусь

в качестве учебного пособия

для студентов учреждений, обеспечивающих получение

высшего образования по специальностям «Искусственный интеллект», «Программное обеспечение информационных технологий»,

«Автоматизированные системы обработки информации»,

«Электронные вычислительные средства»,

«Инженерно-психологическое обеспечение информационных технологий»

Минск БГУИР 2007


УДК 621.3 (075.8)

ББК 22.193 я 73

     Б 28

Р е ц е н з е н т ы :

зав. кафедрой алгоритмики и дискретной математики БГУ,

д-р техн. наук, проф. В. М. Котов;

начальник кафедры систем автоматического управления Военной академии Республики Беларусь, д-р техн. наук, проф. В. А. Куренев

Батура, М. П.

Б 28  Основы алгоритмизации и программирования. Язык Си : учеб. пособие / М. П. Батура, В. Л. Бусько, А. Г. Корбит, Т. М. Кривоносова. – Минск : БГУИР, 2007. – 240 с. : ил.

ISBN  978-985-488-192-8

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

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

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

УДК  621.3 (075.8)

ББК  22.193 я 73

ISBN 978-985-488-192-8  УО «Белорусский государственный

  университет информатики

  и радиоэлектроники», 2007


СОДЕРЖАНИЕ

[1]
ГЛАВА 1. Введение в алгоритмы  

[1.1] 1.1. Этапы решения задач на ЭВМ

[1.2] 1.2. Понятие алгоритма

[1.3] 1.3. Свойства алгоритмов

[1.4] 1.4. Сложность алгоритма

[1.5] 1.5. Способы описания алгоритмов

[1.6] 1.6. Способы реализации алгоритмов

[1.7] 1.7. Пример простейшего линейного процесса

[1.8] 1.7. Пример циклического процесса

[2]
ГЛАВА 2. Базовые средства языка Си

[2.1] 2.1. Алфавит языка Си

[2.2] 2.2. Лексемы

[2.3] 2.3. Идентификаторы и ключевые слова

[2.4] 2.4. Комментарии

[2.5] 2.5. Простейшая программа

[2.6] 2.6. Основные типы данных

[2.7] 2.7. Декларация объектов

[2.8] 2.8. Данные целого типа (integer)

[2.9] 2.9. Данные символьного типа (char)

[2.10] 2.10. Данные вещественного типа (float, double)

[2.11] 2.11. Использование модификаторов при декларации производных типов данных

[3] ГЛАВА 3. Константы в программах

[3.1] 3.1. Целочисленные константы

[3.2] 3.2. Константы вещественного типа

[3.3] 3.3. Символьные константы

[3.4] 3.4. Строковые константы

[4] ГЛАВА 4. Обзор операций

[4.1] 4.1. Операции, выражения

[4.2] 4.2. Арифметические операции

[4.3] 4.3. Операция присваивания

[4.4] 4.4. Сокращенная запись операции присваивания

[4.4.0.1] Смысл записи

[4.4.0.2] Значения

[4.5] 4.5. Преобразование типов операндов арифметических операций

[4.6] 4.6. Операция приведения типа

[4.7] 4.7. Операции сравнения

[4.8] 4.8. Логические операции

[4.9] 4.9. Побитовые логические операции, операции над битами

[4.10] 4.10. Операция «,» (запятая)

[5] ГЛАВА 5. Обзор базовых инструкций языка Си

[5.1] 5.1. Стандартная библиотека языка Си

[5.2] 5.2. Стандартные математические функции

[5.3] 5.3. Функции вывода данных на дисплей

[5.4] 5.4. Функции ввода информации

[5.4.1] Советы по программированию

[6] ЗАДАНИЕ 1. Составление линейных алгоритмов

[6.0.1] Первый уровень сложности

[6.0.2] Второй уровень сложности

[7]
ГЛАВА 6. Составление разветвляющихся алгоритмов

[7.1] 6.1. Краткая характеристика операторов языка Си

[7.2] 6.2. Условные операторы

[7.3] 6.3. Условная операция «? :»

[7.4] 6.4. Оператор выбора альтернатив (переключатель)

[8]
ГЛАВА 7. Составление циклических алгоритмов

[8.1] 7.1. Понятие циклического кода

[8.2] 7.2. Оператор с предусловием while 

[8.3] 7.3. Оператор цикла с постусловием do – while

[8.4] 7.4. Оператор цикла с предусловием и коррекцией  for

[9]
ГЛАВА 8. Операторы и функции передачи управления

[9.1] 8.1. Оператор безусловного перехода goto

[9.2] 8.2. Операторы continue, break и return

[9.3] 8.3. Функции exit и abort

[9.3.1] Советы по программированию

[10] ЗАДАНИЕ 2. Разветвляющиеся алгоритмы

[10.0.1] Первый уровень сложности

[10.0.2] Второй уровень сложности

[11] ЗАДАНИЕ 3. Циклические алгоритмы

[11.0.1] Первый уровень сложности

[11.0.2] Второй уровень сложности

[12] ГЛАВА 9. Указатели

[12.1] 9.1. Определение указателей

[12.2] 9.2. Операция sizeof

[12.3] 9.3. Инициализация указателей

[12.4] 9.4. Операции над указателями

[13] ГЛАВА 10. Массивы

[13.1] 10.1. Понятие массива

[13.2] 10.2. Одномерные массивы

[13.3] 10.3. Связь указателей и массивов

[13.4] 10.4. Строки как одномерные массивы данных типа char

[13.5] 10.5. Указатели на указатели

[13.6] 10.6. Многомерные массивы

[13.7] 10.7. Адресная функция

[13.8] 10.8. Работа с динамической памятью

[13.9] 10.9. Библиотечные функции

[13.10] 10.10. Пример создания одномерного динамического массива

[13.11] 10.11. Пример создания двухмерного динамического массива

[14] ГЛАВА 11. Функции пользователя

[14.1] 11.1. Декларация функции

[14.2] 11.2. Вызов функции

[14.3] 11.3. Передача аргументов в функцию

[14.4] 11.4. Операция typedef

[14.5] 11.5. Указатели на функции

[14.6] 11.6. Рекурсивные функции

[14.7] 11.7. Параметры командной строки функции main

[15] ГЛАВА 12. Классы памяти и область действия объектов

[15.1] 12.1. Классы памяти объектов в языке Cи

[15.2] 12.2. Автоматические переменные

[15.3] 12.3. Статические и внешние переменные

[15.4] 12.4. Область действия переменных

[15.4.1] Советы по программированию

[16] ЗАДАНИЕ 4. Обработка массивов

[16.0.1] Первый уровень сложности

[16.0.2] Второй уровень сложности

[17] ЗАДАНИЕ 5. Функции пользователя

[17.0.1] Первый уровень сложности

[17.0.2] Второй уровень сложности

[18] ГЛАВА 13. Структуры, объединения, перечисления

[18.1] 13.1. Структуры

[18.2] 13.2. Декларация структурного типа данных

[18.3] 13.3. Создание структурных переменных

[18.4] 13.4. Обращение к полям структур

[18.5] 13.5. Вложенные структуры

[18.6] 13.6. Массивы структур

[18.7] 13.7. Размещение структурных переменных в памяти

[18.8] 13.8. Объединения

[18.9] 13.9. Перечисления

[18.10] 13.10. Битовые поля

[19] ГЛАВА 14. Файлы в языке Си

[19.1] 14.1. Открытие файла

[19.2] 14.2. Закрытие файла

[19.3] 14.3. Запись-чтение информации

[19.4] 14.4. Позиционирование в файле

[19.5] 14.5. Дополнительные файловые функции

[19.5.1] Советы по программированию

[20] ЗАДАНИЕ 6. Создание и обработка структур

[20.0.1] Первый уровень сложности

[20.0.2] Второй уровень сложности

[21] ЗАДАНИЕ 7. Создание и обработка файлов

[21.0.1] Первый уровень сложности

[21.0.2] Второй уровень сложности

[22] ГЛАВА 15. Динамические структуры данных

[22.1] 15.1. Линейные списки

[22.2] 15.2. Структура данных СТЕК

[22.2.1] 15.2.1. Алгоритм формирования  стека

[22.2.2] 15.2.2. Алгоритм извлечения элемента из стека

[22.2.3] 15.2.3. Просмотр стека

[22.2.4] 15.2.4. Алгоритм освобождения памяти, занятой  стеком

[22.2.5] 15.2.5. Алгоритм проверки правильности расстановки скобок

[22.3] 15.3. Структура данных ОЧЕРЕДЬ

[22.3.1] 15.3.1. Формирование очереди

[22.3.2] 15.3.2. Алгоритм удаления первого элемента из очереди

[22.4] 15.4. Двунаправленный линейный список

[22.4.1] 15.4.1. Формирование первого элемента

[22.4.2] 15.4.2. Добавление элементов в конец списка

[22.4.3] 15.4.3. Алгоритм просмотра списка

[22.4.4] 15.4.4. Алгоритм поиска элемента в списке по ключу

[22.4.5] 15.4.5. Алгоритм удаления элемента в списке по ключу

[22.4.6] 15.4.6. Алгоритм вставки элемента в список после элемента с указанным ключом

[22.5] 15.5. Нелинейные структуры данных

[22.5.1] 15.5.1. Бинарные деревья

[22.5.2] 15.5.2. Основные алгоритмы работы с бинарным деревом

[22.5.3] 15.5.3. Формирование дерева

[22.5.4] 15.5.4. Вставка нового элемента

[22.5.5] 15.5.5. Удаление узла

[22.5.6] 15.5.6. Алгоритмы обхода дерева

[22.5.7] 15.5.7. Функция просмотра

[22.5.8] 15.5.8. Освобождение памяти

[22.6] 15.6. Построение обратной польской записи

[22.6.1] 15.6.1. Алгоритм, использующий дерево

[22.6.2] 15.6.2. Алгоритм, использующий стек

[22.6.3] 15.6.3. Пример реализации

[22.7] 15.7. Понятие хеширования

[22.7.1] 15.7.1. Хеш-таблица и хеш-функции

[22.7.2] 15.7.2. Примеры хеш-функций

[22.7.3] 15.7.3. Схемы хеширования

[22.7.4] 15.7.4. Примеры реализации схем хеширования

[23] ЗАДАНИЕ 8. Обработка списков

[23.0.1] Вариант 1. Однонаправленные списки

[23.0.2] Вариант 2. Двунаправленные списки

[24] ЗАДАНИЕ 9. Деревья и польская запись

[24.0.1] Вариант 1. Создание и обработка структур типа «дерево»

[24.0.2] Вариант 2. Создание и использование польской записи

[25]
ГЛАВА 16. Переход к ООП

[25.1] 16.1. Потоковый ввод-вывод

[25.2] 16.2. Управление выводом

[25.3] 16.3. Проблема ввода-вывода кириллицы в среде Visual C++

[25.4] 16.4. Операции new и delete

[25.5] 16.5. Дополнительные возможности при работе с пользовательскими функциями

[25.6] 16.6. Шаблоны функций

[25.6.1] Советы по программированию

[26] ЗАДАНИЕ 10. Перегрузка функций

[26.0.1] Первый уровень сложности

[26.0.2] Второй уровень сложности

[27]
Таблицы символов ASCII

[28] Операции языка Си

[29]

[30] Возможности препроцессора

[31] Интегрированная среда программирования Visual C++

[32]  

[33] Некоторые возможности отладчика Visual C++

[34] Некоторые возможности графической подсистемы

[34.1] 6.1. Основные понятия

[34.2] 6.2. Контекст устройства

[34.3] 6.3. Примитивы GDI

[34.4] 6.4. Пример вывода текста

[34.5] 6.5. Получение описателя контекста устройства

[34.6] 6.6. Основные инструменты графической подсистемы

[34.7] 6.7. Закрашивание пустот

[34.8] 6.8. Рисование линий и кривых

[34.9] 6.9. Пример изображения графика функции sin

[34.10] 6.10. Рисование замкнутых фигур

[34.11] 6.11. Функция Polygon и режим закрашивания многоугольника

[34.12] 6.12. Пример отображения линий

[34.13] 6.13. Управление областями вывода и отсечением

[34.14] 6.14. Растровая графика

[35] ЗАДАНИЕ 11. Создание графических изображений

[36] ЛИТЕРАТУРА


ПРЕДИСЛОВИЕ

Алгоритмический язык Си был разработан в 1972 г. сотрудником фирмы AT&T Bell Laboratory Денисом Ритчи на базе языка В (автор К.Томпсон), который в свою очередь основывался на языке системного программирования BCPL. Первая версия языка была опубликована в книге авторов Б. Кернигана и Д. Ритчи и получила название стандарт K&R. Минимальная стандартная реализация, поддерживаемая любым компилятором, содержала всего 27 ключевых слов. Началось успешное развитие языка и, чтобы избежать путаницы, Американский институт стандартизации (American National Standart Institute) ввел в 1983 г. общий стандарт языка – ANSI-стандарт.

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

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

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

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

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

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

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

Текст программы, записанный на языке высокого уровня и введенный с помощью клавиатуры в память компьютера, – исходный модуль. Программы, написанные в среде программирования, предназначенной для языка Си, например Turbo C, имеют расширение *.с. Расширение *.cpp имеют программы, написанные в интегрированных средах Borland C++, Visual C++, Builder C++, предназначенных для написания программ как на языке Си, так и на языке С++.

Большинство трансляторов языка Си – компиляторы.

Результат обработки исходного модуля компилятором – объектный модуль (расширение *.obj). На этом этапе компилятор выделяет лексемы (элементарные конструкции языка), затем на основе грамматики распознает выражения и операторы, построенные из этих лексем. При этом компилятор выявляет синтаксические ошибки и, в случае их отсутствия, создает объектный модуль.

Исполняемый (абсолютный, загрузочный) модуль создает вторая специальная программа – «компоновщик». Ее еще называют редактором связей (Linker). Она и создает загрузочный модуль (расширение *.exe) на основе одного или нескольких объектных модулей – это программный модуль, представленный в форме, пригодной для выполнения.

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


ГЛАВА 1. Введение в алгоритмы  

1.1. Этапы решения задач на ЭВМ

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

1) математическая или информационная формулировка задачи;

2) выбор численного или иного метода решения поставленной задачи;

3) построение алгоритма решения поставленной задачи;

4) выбор языка программирования и запись построенного алгоритма по его правилам, т.е. написание текста программы;

5) отладка программы – это процесс обнаружения, локализации и устранения возможных ошибок;

6) выполнение программы, т.е. получение требуемого результата.

Рассмотрим более подробно некоторые наиболее важные из приведенных этапов.

1.2. Понятие алгоритма

Понятие алгоритма занимает центральное место в современной математике и программировании.

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

Рассмотрим вначале некоторые наиболее важные (фундаментальные) понятия программирования.

1. Действие – это некоторая операция, имеющая конкретную продолжительность и приводящая к совершенно конкретному результату.

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

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

4. Если действие можно разложить на составные части, то его называют процессом (или вычислением).

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

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

В общем, алгоритм – строгий и четкий набор правил, определяющий последовательность действий, приводящих к достижению поставленной цели.

1.3. Свойства алгоритмов

Дискретность – значения новых величин (данных) вычисляются по определенным правилам из других величин с уже известными значениями.

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

Результативность (конечность) – алгоритм решает поставленную задачу за конечное число шагов.

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

1.4. Сложность алгоритма

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

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

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

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

Временная сложность алгоритма – это функция, которая каждой входной длине слова n ставит в соответствие максимальное (для всех конкретных задач длиной n) время, затрачиваемое алгоритмом на ее решение. 

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

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

Задача считается труднорешаемой, если для нее не удается построить полиномиального алгоритма. Это утверждение не является категорическим, поскольку известны задачи, в которых достаточно эффективно работают и экспоненциальные алгоритмы. Примером может служить симплекс-метод, который успешно используется при решении задач линейного программирования, имея функцию сложности f(n) = 2n. Однако подобных примеров не очень много, и общей следует признать ситуацию, когда эффективно исполняемыми можно считать полиномиальные алгоритмы с функциями сложности n, n2 или n3.

Например, при решении задачи поиска нужного элемента из n имеющихся в худшем варианте сложность равна n; если же оценить среднюю трудоемкость (продолжительность поиска), то она составит (n+1)/2 – в обоих случаях функция сложности оказывается линейной n.

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

1.5. Способы описания алгоритмов

Существует несколько способов описания алгоритмов. Наиболее распространенные способы – это словесное и графическое описания алгоритма.

Словесное описание алгоритма

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

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

1) шаг (этап) обработки (вычисления) значений данных – «=»;

2) проверка логического условия: если (условие) истинно, то выполнить действие 1, иначе – действие 2;

3) переход (передача управления) к определенному шагу (этапу) N.

Для примера рассмотрим алгоритм решения квадратного уравнения вида ax2+bx+c = 0:

1) ввод исходных данных a, b, c (a,b,c  0);

2) вычислить дискриминант D = b2 – 4ac ;

3) если D < 0, то перейти к п. 6, сообщив, что действительных корней нет;

4) иначе, если D 0, вычислить х1= (–b+)/(2a) и  х2 = (–b–)/(2a);

5) вывести результаты х1 и х2 ;

6) конец.

Графическое описание алгоритма

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

Правила изображения фигур сведены в единую систему программной документации (дата введения последнего стандарта ГОСТ 19.701.90 – 01.01.1992).

По данному ГОСТу графическое изображение алгоритма – это схема данных, которая отображает путь данных при решении задачи и определяет этапы их обработки.

Схема данных состоит из следующих элементов:

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

Рассмотрим основные символы для изображения схемы алгоритма.

Символы ввода-вывода данных:

данные ввода-вывода, если носитель не определен;

ручной ввод с устройства любого типа, например с клавиатуры;

отображение данных в удобочитаемой форме на устройстве, например дисплее.

Символы процесса:

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

предопределенный процесс – отображение группы операций, которые определены в другом месте, например в подпрограмме (функции);

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

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

Специальные символы

Соединитель используется при обрыве линии и продолжении ее в другом месте (необходимо присвоить название).

Терминатор – вход из внешней среды или выход во внешнюю среду (начало или конец схемы программы).

Комментарий.

1.6. Способы реализации алгоритмов

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

1) линейные (единственное направление выполнения);

2) разветвляющиеся (направление выполнения определяет условие);

3) циклические (отдельные участки вычислений выполняются многократно).

Любой циклический процесс включает в себя участок с разветвлением и может быть простым и сложным (вложенным).

Для решения вопроса о том, сколько раз нужно выполнить цикл, используется анализ переменной, которую называют параметром цикла.

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

1.7. Пример простейшего линейного процесса

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

где x > 0 – вещественное, m – целое.

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

Для того чтобы не было «длинных» операторов, исходное выражение полезно разбить на ряд более простых. В нашей задаче предлагается схема вычислений, представленная на рис. 1.1.

Рис. 1.1. Схема линейного процесса

Она содержит ввод и вывод исходных данных, линейный вычислительный процесс, вывод полученного результата. Заметим, что выражение  вычисляется только один раз. Введя дополнительные переменные a, b, c, мы разбили сложное выражение на ряд более простых.

1.7. Пример циклического процесса

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

.

Схема алгоритма, приведенная на рис. 1.2, реализует циклический процесс, в состав которого (в блоке проверки |E|< eps) входит участок разветвления.

 

Рис. 1.2. Схема циклического алгоритма


ГЛАВА 2. Базовые средства языка Си 

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

При создании программ разработчик может допустить следующие ошибки: синтаксические и логические.

Синтаксические ошибки – это результат нарушения формальных правил написания программы на конкретном языке программирования.

Логические ошибки разделяются, в свою очередь, на ошибки алгоритма и семантические ошибки.

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

Причина семантической ошибки – неправильное понимание смысла (семантики) операторов выбранного языка программирования.

2.1. Алфавит языка Си

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

Каждому из множества значений, определяемых одним байтом (от 0 до 255), в таблице знакогенератора ЭВМ ставится в соответствие символ. По кодировке фирмы IBM символы с кодами от 0 до 127, образующие первую половину таблицы знакогенератора, построены по стандарту ASCII и одинаковы для всех компьютеров, вторая половина символов (коды 128 – 255) может отличаться и обычно используется для размещения символов национального алфавита. Коды 176 – 223 отводятся под символы псевдографики, а коды 240 – 255 – под специальные знаки (прил. 1).

Алфавит языка Си включает:

– прописные и строчные буквы латинского алфавита и знак подчеркивания (код 95);

– арабские цифры от 0 до 9;

– специальные символы, смысл и правила использования которых будем рассматривать по тексту;

– пробельные (разделительные) символы: пробел, символы табуляции, перевода строки, возврата каретки, новой страницы и новой строки.

2.2. Лексемы

Из символов алфавита формируются лексемы (или элементарные конструкции) языка – минимальные значимые единицы текста в программе:

– идентификаторы;

– ключевые (зарезервированные) слова;

– знаки операций;

– константы;

– разделители (скобки, точка, запятая, пробельные символы).

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

2.3. Идентификаторы и ключевые слова

Идентификатор (ID) – это имя программного объекта (константы, переменной, метки, типа, функции и т.д.). В идентификаторе могут использоваться латинские буквы, цифры и знак подчеркивания; первый символ ID – не цифра; пробелы внутри ID не допускаются.

Длина идентификатора определяется выбранной версией среды программирования. Например, в среде Borland C++ 6.0 идентификаторы могут включать любое число символов, из которых воспринимаются и используются только первые 32 символа. Современная тенденция – снятие ограничений длины идентификатора.

При именовании объектов следует придерживаться общепринятых соглашений:

ID переменных и функций обычно пишутся строчными (малыми) буквами – index, max();

ID типов пишутся с большой буквы, например, Spis, Stack;

ID констант (макросов) – большими буквами – INDEX, MAX_INT;

– идентификатор должен нести смысл, поясняющий назначение объекта в программе, например, birth_date – день рождения, sum – сумма;

– если ID состоит из нескольких слов, как, например, birth_date, то принято либо разделять слова символом подчеркивания, либо писать каждое следующее слово с большой буквы – birthDate.

В Си прописные и строчные буквы – различные символы. Идентификаторы Name, NAME, name – различные объекты.

Ключевые (зарезервированные) слова не могут быть использованы в качестве идентификаторов.

Список ключевых слов, определенных в стандарте ANSI Cи:

auto

do

goto

signed

unsigned

break

double

if

sizeof

void

case

else

int

static

volatile

char

enum

long

struct

while

const

extern

register

switch

continue

float

return

typedef

default

for

short

union

2.4. Комментарии

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

В Си комментарии ограничиваются парами символов /* и */, а в С++ был введен вариант комментария, который начинается символами // и заканчивается символом перехода на новую строку.

2.5. Простейшая программа

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

Общая структура программы на языке Си имеет вид:

<директивы препроцессора>

<определение типов пользователя – typedef>

<описание прототипов функций>

<определение глобальных переменных>

<функции>

В свою очередь, каждая функция имеет следующую структуру:

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

{ – начало функции

код функции

  } – конец функции

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

Функции не могут быть вложенными друг в друга.

Рассмотрим кратко основные части общей структуры программ.

Перед компиляцией программа обрабатывается препроцессором (прил. 3), который работает под управлением директив.

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

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

#include < ID_файла.h>

где h – расширение заголовочных файлов.

Если идентификатор файла заключен в угловые скобки (< >), то поиск данного файла производится в стандартном каталоге, если – в двойные кавычки (” ”), то поиск файла производится в текущем каталоге.

К наиболее часто используемым библиотекам относятся:

stdio.h – содержит стандартные функции файлового ввода-вывода;

math.h – математические функции;

conio.h – функции для работы с консолью (клавиатура, дисплей).

Второе основное назначение препроцессора – обработка макроопределений.  Макроподстановка определить  (define) имеет общий вид

#define  ID   строка

Например: #define  PI   3.1415927

– в ходе препроцессорной обработки программы идентификатор PI везде будет заменяться значением 3.1415927.

Рассмотрим пример, позволяющий понять простейшие приемы программирования на языке Си:

#include <stdio.h>    

void main(void)  

{    // Начало функции main

printf(“ Высшая оценка знаний – 10 !”);   

}    // Окончание функции main

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

Для начала будем использовать функцию main без параметров и не возвращающую значения.

Код функции представляет собой набор инструкций, каждая из которых оканчивается символом «;». В нашем примере одна инструкция – функция printf, выполняющая вывод данных на экран, в данном случае – указанную фразу.

__________________________________________________________________

Приемы отладки в среде программирования Visual C++ 6.0 рассматриваются в прил. 5.

__________________________________________________________________

2.6. Основные типы данных

Данные в языке Си разделяются на две категории: простые (скалярные), будем их называть базовыми, и сложные (составные) типы данных.

Тип данных определяет:

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

Основные типы базовых данных: целый – int (integer), вещественный с одинарной точностью – float  и символьный – char (character).

В свою очередь, данные целого типа могут быть короткими – short, длинными – long и беззнаковыми – unsigned, а вещественные – с удвоенной точностью – double.

Сложные типы данных – массивы, структуры – struct, объединения – union, перечисления – enum.

Данные целого и вещественного типов находятся в определенных диапазонах, т.к. занимают разный объем оперативной памяти (табл. 2.1).

Таблица 2.1

Тип данных

Объем памяти (байт)

Диапазон значений

сhar

1

–128    … 127

int

2 (4)*

–32768 … 32767

short

1 (2)*

–32768 … 32767(–128 … 127)

long

4

–2147483648 … 2147483647

unsigned int

4

0 … 65535

unsigned long

4

0 … 4294967295

float

4

3,1410–38 … 3,141038

double

8

1,710–308 … 1,710308

long double

10

3,410–4932 … 3,4104932

* Размер памяти зависит от разрядности процессора, для 16-разрядных объем памяти определяется первой цифрой, для 32-разрядных – второй.

                        

2.7. Декларация объектов 

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

– описание, не приводящее к выделению памяти;

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

Кроме констант, заданных в исходном тексте, все объекты программы должны быть явно декларированы по следующему формату:

<атрибуты> <список ID объектов>;

элементы списка ID объектов разделяются запятыми,  а атрибуты  – разделителями, например: int i, j, k; float a, b;

Объекты программы могут иметь следующие атрибуты:

класс памяти – характеристика способа размещения объектов в памяти (статическая, динамическая); определяет область видимости и время жизни переменной (по умолчанию – auto), данные атрибуты будут рассмотрены в гл. 12;

тип – тип будущих значений декларируемых объектов (по умолчанию устанавливается тип int).

Класс памяти и тип – атрибуты необязательные и при отсутствии одного из них (но не обоих одновременно) устанавливаются атрибуты по умолчанию.

Примеры декларации простых объектов:

    int i, j, k;       char r;       double gfd;

Рассмотрим основные базовые типы данных более подробно.

2.8. Данные целого типа (integer)

Тип int – целое число, обычно соответствующее естественному размеру целых чисел. Квалификаторы short и long указывают на различные размеры и определяют объем памяти, выделяемый под них (см. табл. 2.1), например:   

short  x;

long  x;

unsigned x = 8;  – декларация с инициализацией числом 8;

атрибут int в этих случаях может быть опущен.

Атрибуты signed и unsigned показывают, как интерпретируется старший  бит  числа – как  знак  или  как часть числа:

int

Знак

Значение числа

15

14 13 12 11 10 9 8 7 6 5 4 3 2 1  0

 – номера бит

 

unsigned int

Значение числа

15                                                 0

long

Знак

Значение числа

31

30                                                 0

unsigned long

Значение числа

31                                                 0

2.9. Данные символьного типа (char)

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

Для персональных компьютеров (ПК) наиболее распространена ASCII (American Standard Code for Information Interchenge) таблица кодов (см. прил. 1). Данные типа char рассматриваются компилятором как целые, поэтому возможно использование signed char: величины со знаком (по умолчанию) – символы с кодами от –128 до +127 и unsigned char – беззнаковые символы с кодами от 0 до 255. Этого достаточно для хранения любого символа из 256-символьного набора ASCII. Величины типа char применяют еще и для хранения целых чисел из указанных диапазонов.

Примеры: char res, simv1, simv2;

char let = 's'; – декларация символьной переменной с инициализацией символом s.

2.10. Данные вещественного типа (float, double)

Данные вещественного типа в памяти занимают (табл. 2.2): float – 4 байта (одинарная точность), double (удвоенная точность) – 8 байт; long double (повышенная точность) – 10 байт. Для размещения данных типа float обычно 8 бит выделено для представления порядка и знака и 24 бита под мантиссу.

Таблица 2.2

Тип

Точность (мантисса)

Порядок

float    (4 байта)

7 цифр после запятой

38

double   (8 байт)

15

308

long double   (10 байт)

19

4932

Типы данных с плавающей десятичной точкой хранятся в оперативной памяти иначе, чем целочисленные. Внутреннее представление вещественного числа состоит из двух частей: мантиссы и порядка (см. разд. 3.2 «Константы вещественного типа»). В IBM совместимых ПК, как вы уже знаете, переменная типа float занимает 4 байта, из которых один двоичный разряд отводится под знак мантиссы, 8 разрядов под порядок и 23 под мантиссу. Мантисса – это число больше единицы и меньше двух. Поскольку старшая цифра мантиссы всегда равна единице, то ее не хранят.

Для величин типа doublе, занимающих 8 байт, под порядок и мантиссу отводится 11 и 52 разряда соответственно. Длина мантиссы определяет точность числа, а  порядок – его диапазон. Как можно видеть из приведенных выше таблиц, при одинаковом количестве байт, отводимом под величины типа float и long int, диапазоны их допустимых значений сильно различаются из-за внутренней формы представления значений таких данных.

При переносе программы с одной платформы на другую нельзя делать предположений, например, о типе int, так как для оперативной системы (ОС) MS DOS этот тип имеет размер в два байта, а для ОС Windows 9X – четыре байта. В стандарте ANSI поэтому диапазоны значений для основных типов не задаются, а определяются только соотношения между их размерами, например:

sizeof (float) < sizeof (double) < sizeof (long double) ,

sizeof (char) < sizeof (short) < sizeof (int) < sizeof (long) ,

где операция sizeof – возвращает количество байт для указанного аргумента – скалярного типа данных.

2.11. Использование модификаторов при декларации производных типов данных

Ключевые слова int, float, char и т.д. называют конечными атрибутами декларации объектов программы. При декларации так называемых производных объектов используют еще дополнительные – промежуточные атрибуты или, как их иногда называют, «модификаторы».

К символам модификации текущего типа относятся:

– символ * перед идентификатором, обозначающий декларацию указателя на объект исходного типа (левый промежуточный атрибут);

– символы [ ] после идентификатора объекта – декларация массива объектов;

– символы ( ) после идентификатора объекта – декларация функции (правые промежуточные атрибуты).

Допускается использование более одного модификатора типа с учетом следующих правил:

1) чем ближе модификатор к ID объекта, тем выше его приоритет;

2) при одинаковом расстоянии от идентификатора объекта модификаторы [  ] и (  ) обладают приоритетом перед атрибутом звездочка *;

3) дополнительные круглые скобки позволяют изменить приоритет объединяемых ими элементов описания;

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

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

Примеры декларации объектов с конечным атрибутом int:

int a;  – переменная типа int;     

int a[5];    – массив из пяти элементов типа int;     

int *a;    – указатель на объект типа int;     

int **a;    – указатель на указатель на объект типа int;     

int *a[5];    – массив из пяти указателей на элементы типа int;     

int (*a)[10];     – указатель на массив из десяти элементов типа int;     

int *a[3][4];     – 3-элементный массив указателей на одномерные целочисленные массивы  по четыре элемента каждый;

int a[5][2];      – двухмерный массив элементов типа int;      

int a(void);    – функция без параметров, возвращающая значение типа int;      

int *a(void);         – функция без параметров, возвращающая указатель на элемент типа int;         

int (*a)(void);       – указатель на функцию без параметров, возвращающую значение типа int;          

int *a(void)[6]; – функция без параметров, возвращающая указатель на массив элементов типа int;        

int *a [4](void); – массив указателей на функцию без параметров, возвращающую значение типа int.         

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

ГЛАВА 3. Константы в программах

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

– самоопределенные арифметические константы целого и вещественного типов, символьные и строковые данные;

– идентификаторы массивов и функций;

– элементы перечислений.

3.1. Целочисленные константы

Общий формат записи:  n  (+ обычно не ставится).

Десятичные константы – это последовательность цифр 0...9, первая из которых не должна быть 0. Например, 22 и 273 – обычные целые константы, если нужно ввести длинную целую константу, то указывается признак L(l) – 273L (273l).  Для такой константы будет отведено – 4 байта. Обычная целая константа, которая слишком длинна для типа int, рассматривается как long.

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

Восьмеричные константы – это последовательность цифр от 0 до 7, первая из которых должна быть 0, например: 0208 = 1610.

Шестнадцатеричные константы – последовательность цифр от 0 до 9 и букв от A до F (a...f), начинающаяся символами 0Х (0х), например: 0X1F16 (0х1f)16 =  3110.

Восьмеричные и шестнадцатеричные константы могут также заканчиваться буквой L(l) – long, например, 020L или 0X20L.

Примеры целочисленных констант:

1992   777   1000L – десятичные;

0777  00033  01l  – восьмеричные;

0x123  0X00ff  0xb8000l  – шестнадцатеричные.

3.2. Константы вещественного типа

Данные константы размещаются в памяти в формате double, а во внешнем представлении могут иметь две формы:

1) с фиксированной десятичной точкой, формат записи: n.m, где n, m – целая и дробная части числа;

2) с плавающей десятичной точкой (экспоненциальная форма) представляется в виде мантиссы и порядка. Мантисса записывается слева от знака экспоненты (Е или е), а порядок – справа. Значение константы определяется как произведения мантиссы и числа 10, возведенного в указанную в порядке степень.

Общий формат таких констант: n.mEp, где n, m – целая и дробная части числа, р – порядок;  0.xxxEp – нормализованный вид, например, 1,2510–8 =  0.125E–7.

Примеры констант с фиксированной и плавающей точками:

1.0    –3.125 100е–10  0.12537е+12.

Пробелы внутри чисел не допускаются, а для отделения целой части числа от дробной используется точка. Можно опустить нулевую дробную или целую части числа, но не обе сразу, например, 1.0 1. или 0.5 .5 .

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

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

Символьная константа – это символ, заключенный в одинарные кавычки:  'A', 'х' (тип char занимает в памяти один байт).

Также используются специальные последовательности символов –  управляющие (escape) последовательности:

\n

– новая строка;

\t

– горизонтальная табуляция;

\b

– шаг назад;

\r

– возврат каретки;

\v

– вертикальная табуляция;

\f

– перевод формата (переход на новую строку);

\\

– обратный слеш;

\'

– апостроф;

\"

– кавычки;

\0

– символ «пусто», не путать с символом ‘0’.

Символьная константа '\0' – это нулевой байт, каждый бит которого равен нулю.

При присваивании символьным переменным значений констант значения констант заключаются в апострофы, например:

 char ss = ‘У’;

Текстовые символы непосредственно вводятся с клавиатуры, а специальные и управляющие – представляются в исходном тексте парами символов, например: \\ ,    \'  ,  \"  .

Примеры символьных констант: 'А',   '9',   '$',   '\n'.

3.4. Строковые константы

Строковая константа представляет собой последовательность символов кода ASCII, заключенную в кавычки (”). Во внутреннем представлении к строковым константам добавляется пустой символ '\0', который не является цифрой 0, на печать не выводится (в таблице кодов ASCII имеет код = 0) и является признаком окончания строки.

Кавычки не являются частью строки, а служат только для ее ограничения. Строка в языке Си представляет собой массив, состоящий из символов. Внутреннее представление константы "1234ABC":  '1' '2' '3' '4' 'A' 'B' 'C' '\0'  .

Примеры строковых констант:

    "Система", "\n\t Аргумент \n", "Состояние \"WAIT \" "  .

Строковые константы еще называют строковыми литералами.

В конец строковой константы компилятор автоматически помещает нуль-символ.

Длинную строковую константу можно разбить на несколько, используя символ переноса – обратный слеш (\).  Например:

Вы поступили и \

учитесь на факультете информационных технологий \

Белорусского государственного университета \

информатики и радиоэлектроники

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

ГЛАВА 4. Обзор операций

4.1. Операции, выражения

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

Знак операции – это один или более символов, определяющих действие над операндами, т.е. операции задают действия, которые необходимо выполнить. Внутри знака операции пробелы не допускаются.

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

Большинство операций выполняются слева направо, например, a+b+  (a+b)+c. Исключение составляют унарные операции, операции присваивания и условная операция (?:), которые выполняются справа налево.

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

– операция доступа к полям структур и объединений при помощи идентификаторов «.» – точка;

– операция доступа к полям структур и объединений при помощи указателей «–>» – стрелка;

– операция [ ] индексации, используемая при декларации массива и  обращении к его элементам;   

– операция ( ) обращения к функции.

Первичные операции будут рассмотрены в соответствующих разделах.

Полный список операций с указанием их приоритетов приводится в прил. 2.

4.2. Арифметические операции

Обозначения арифметических операций:

+  (сложение); – (вычитание); / (деление, для int операндов – с отбрасыванием остатка); * (умножение); % (остаток от деления целочисленных операндов со знаком первого операнда – деление «по модулю»).

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

Порядок выполнения действий в арифметических выражениях следующий: выражения в круглых скобках; операции *, /, %; операции +, –.

Унарные операции «знак числа» (+, –) обладают самым высоким приоритетом и определены для операндов числовых типов (имеющих числовой результат), при этом «+» носит только информационный характер, «–» меняет знак операнда на противоположный (неадресная операция).

Операции  *, /, % обладают высшим приоритетом над операциями +, –, поэтому при записи сложных выражений нужно использовать общепринятые математические правила:   x+y*za /(b+с) , т.е. использовать круглые скобки.

4.3. Операция присваивания

Формат операции присваивания:

Операнд_1  =  Операнд_2 ;

Операндом_1 (левый операнд) может быть только переменная. Левый операнд операции присваивания получил название L–значение, (Lvalue, Leftvalue) – адресное выражение. Так в Си называют любое выражение, адресующее некоторый участок оперативной памяти, в который можно записать некоторое значение. Переменная – это частный случай адресного выражения.

Операндом_2 (правый операнд) могут быть: константа, переменная или любое выражение, составленное в соответствии с синтаксисом языка Си. Правый операнд операции присваивания назвали R–значение, (R–value, Right–value).

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

    int i, j, k;

    float x, y, z;

     ...

    i = j = k = 0;               k = 0, j = k, i = j;

    x = i + (y = 3) – (z = 0);      z = 0, y = 3, x = i + yz;

Примеры недопустимых выражений:

– присваивание константе:   2 = x + y;

– присваивание функции:       getch() = i;   

– присваивание результату операции:     (i + 1) = 2 + y;  

4.4. Сокращенная запись операции присваивания

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

1) вместо записи: v = v # e;   

где # – любая арифметическая операция (операция над битовым представлением операндов), рекомендуется использовать запись  v #= e;

Например,  i = i + 2;    i += 2;  (знаки операций – без пробелов);

2) вместо записи: x = x # 1;

где  # – символы, обозначающие операцию инкремента (+1), либо декремента (–1),  x – целочисленная переменная (или переменная-указатель), рекомендуется использовать запись:

     ##x; – префиксную,    или  x##;  – постфиксную.

Если эти операции используются в чистом виде, то различий между постфиксной и префиксной формами нет. Если же они используются в выражении, то в префиксной форме (##x) сначала значение x изменится на 1, а затем полученный результат будет использован в выражении; в постфиксной форме (x##) – сначала значение переменной х используется в выражении, а затем изменится на 1. Операции над указателями будут рассмотрены в разд. 9.4.

Пример 1:

Пример 2:

int i, j, k;

Смысл записи

int n, a, b, c, d;

Значения

float x, y;

n = 2; a = b = c = 0;

...       

a = ++n;     

n=3, a=3

х *= y;     

x = x*y;

a += 2;     

a=5

i += 2;     

i = i + 2;

b = n++;     

b=3, n=4

x /= y+15;  

x = x/(y + 15);

b –= 2;     

b=1

--k;

k = k – 1;

c = --n;     

n=3, c=3

k--;

k = k – 1;

c *= 2;     

c=6

j = i++;    

j = i;     i = i + 1;

d = n--;     

d=3, n=2

j = ++i;   

i = i + 1;     j = i;

d %= 2;     

d=1

4.5. Преобразование типов операндов арифметических операций

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

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

short, char

  int

  unsigned

  long

  double

float

  double

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

– значения типов char и short всегда преобразуются в int;

– если один из операндов имеет тип double, то и другой преобразуется в double;

– если один из операндов long, то другой преобразуется в long.

 Внимание. Результатом операции 1/3 будет значение 0, чтобы избежать такого рода ошибок, необходимо явно изменить тип хотя бы одного операнда, т.е. записать, например:  1./3 .

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

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

Пусть: float x; int i;  тогда и  x = i;   и  i = x; приводят к преобразованиям, причем float преобразуется в int отбрасыванием дробной части.

Тип double  преобразуется в float округлением.

Длинное целое преобразуется в более короткое целое и char посредством отбрасывания бит в старших разрядах.

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

4.6. Операция приведения типа

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

 (тип) выражение;

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

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

double x;

int n = 6, k = 4;

x = (n + k)/3;    x = 3, т.к. дробная часть будет отброшена;

x = (double)(n + k)/3;    x = 3.333333 – использование операции приведения типа позволило избежать округления результата деления целочисленных операндов.

4.7. Операции сравнения

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

==  –  равно или эквивалентно;      !=  –  не равно;

<   –  меньше;          <=  –  меньше либо равно;

>   –  больше;          >=  –  больше либо равно.

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

Общий вид операций отношений:

Операнд_1   Знак операции  Операнд_2

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

Значения арифметических выражений перед сравнением вычисляются и преобразуются к одному типу.

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

В языке Си нет логического типа данных. Результат операции отношения имеет значение 1, если отношение истинно, или в результате вычислений получено не нулевое значение, воспринимаемое компилятором Си как истина (true), или 0 – в противном случае, т.е. – ложно (false). Следовательно, операция отношения может использоваться в любых арифметических выражениях.

Операции сравнения на равенство и неравенство имеют меньший приоритет, чем остальные операции отношений.

Примеры использования операций отношений:

y > 0  ,   x == y  ,  x != 2  .

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

4.8. Логические операции

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

!  – отрицание (логическое «НЕТ»);

&&  – конъюнкция (логическое «И»);

||  – дизъюнкция (логическое «ИЛИ»).

Операндами (выражениями) логических операций могут быть любые скалярные типы. Ненулевое значение операнда трактуется как «истина», а нулевое – «ложь». Результатом логической операции, как и в случае операций отношения, может быть 1 или  0.

Общий вид операции отрицания

      ! выражение

Примеры использования операции отрицания:

     !0      1

     !5      0

     x = 10;

   ! (x > 0)   0

Общий вид операций конъюнкции и дизъюнкции:

Выражение_1  знак операции  Выражение_2

Особенность операций конъюнкции и дизъюнкции – экономное последовательное вычисление выражений-операндов:

– если выражение_1 операции «конъюнкция» ложно, то результат операции – ноль и выражение_2 не вычисляется;

– если выражение_1 операции «дизъюнкция» истинно, то результат операции – единица и выражение_2 не вычисляется.

Например:

> 0 && = 7 истина, если оба выражения истинны;

> 0 || = 7   истина, если хотя бы одно выражение истинно.

Старшинство операции «И» выше, чем «ИЛИ» и обе они младше операций отношения и равенства.

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

0 < < 100     0 < x &&  < 100 ;

 x > 0, y  1    x > 0 && y <=1 .

Учет этих свойств очень существенен для написания правильно работающих программ.

4.9. Побитовые логические операции, операции над битами

В языке Си предусмотрен набор операций для работы с отдельными битами. Эти операции нельзя применять к переменным вещественного типа.

Обозначения операций над битами:

~   – дополнение (унарная операция); инвертирование (одноместная операция);

&   – побитовое «И» – конъюнкция;

|  – побитовое включающее «ИЛИ» – дизъюнкция;

^   – побитовое исключающее «ИЛИ» – сложение по модулю 2;

>>  – сдвиг вправо;

<< – сдвиг влево.

Общий вид операции инвертирования (поразрядное отрицание):

     ~ выражение

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

Остальные операции над битами имеют вид:

Выражение_1  знак операции  Выражение_2

Операндами операций над битами могут быть только выражения, приводимые к целому типу.  Операции (~, &, |, ^) выполняются поразрядно над всеми битами операндов (знаковый разряд особо не выделяется):

~0xF0     x0F

0xFF & 0x0F    x0F

0xF0 | 0x11     xF1

0xF4 ^ 0xF5   x01

Операция & часто используется для маскирования некоторого множества бит. Например, оператор w = n & 0177 передает в w семь младших бит n, полагая остальные равными нулю.

Операции сдвига выполняются также для всех разрядов с потерей выходящих за границы бит.

Операция (|) используется для включения бит w = x | y, устанавливает в единицу те биты в x, которые равны 1 в y.

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

     0x81 << 1    0x02

     0x81 >> 1     0x40

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

Унарная операция (~) дает дополнение к целому, т.е. каждый бит со значением 1 получает значение 0 и наоборот.

Операции сдвига  << и >> применяются к целочисленным операндам и осуществляют соответственно сдвиг вправо (влево) своего левого операнда на число позиций, задаваемых правым операндом, например, x << 2 сдвигает x влево на две позиции, заполняя освобождающиеся биты нулями (эквивалентно умножению на 4).

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

    x << 1     x*2;  x >> 1     x/2 ;

    x << 3     x*8 .

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

Операции сдвига не учитывают переполнение и потерю значимости.

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

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

~  – дополнение;    |  – объединение;     &  – пересечение.

Простейшее применение – проверка нечетности целого числа:

int i;

...

      if ( i & 1) printf (" Значение i четно!");

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

4.10. Операция «,» (запятая)

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

 выражение_1,  …, выражение_N;

выражения 1, 2,…, N вычисляются последовательно друг за другом и результатом операции становится значение последнего выражения N, например:

 m = ( i = 1, j = i ++, k = 6, n = i + j + k );

получим последовательность вычислений: i =1, j = i =1, i = 2, = 6, = 2+1+6, и в результате  = 9.

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

ГЛАВА 5. Обзор базовых инструкций языка Си

5.1. Стандартная библиотека языка Си

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

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

Рассмотрим наиболее часто используемые функции из стандартной библиотеки языка Си.

5.2. Стандартные математические функции

Математические функции языка Си декларированы в файлах math.h и stdlib.h.

В приведенных здесь функциях аргументы и возвращаемый результат имеют тип double. Аргументы тригонометрических функций должны быть заданы в радианах (2π радиан = 360o).

Математическая функция

ID функции в языке Си

sqrt(x)

|x|

fabs(x)

ex

exp(x)

xy

pow(x,y)

ln(x)

log(x)

lg10(x)

log10(x)

sin(x)

sin(x)

cos(x)

cos(x)

tg(x)

tan(x)

arcsin(x)

asin(x)

arccos(x)

acos(x)

arctg(x)

atan(x)

arctg(x / y)

atan2(x)

sh(x)=0.5 (ex–e-x)

sinh(x)

ch(x)=0.5 (ex+e-x)

cosh(x)

tgh(x)

tanh(x)

остаток от деления x на y

fmod(x,y)

наименьшее целое >=x

ceil(x)

наибольшее целое <=x

floor(x)

5.3. Функции вывода данных на дисплей

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

Декларации функций ввода/вывода, как уже упоминалось, приведены в заголовочном файле stdio.h.

Для вывода информации на экран монитора (дисплей) в языке Си чаще всего используются функции: printf() и puts().

Формат функции форматного вывода на экран:

printf(  управляющая строка , список объектов вывода);

В управляющей строке, заключенной в кавычки, записывают: поясняющий текст, который выводится на экран без изменения (комментарии), список модификаторов форматов, указывающих компилятору способ вывода объектов (признак модификатора формата – символ %) и специальные символы, управляющие выводом (признак – символ \).

В списке объектов вывода указываются идентификаторы печатаемых объектов, разделенных запятыми: переменные, константы или выражения, вычисляемые перед выводом.

Количество и порядок следования форматов должен совпадать с количеством и порядком следования выводимых на экран объектов.

Функция printf выполняет вывод данных в соответствии с указанными форматами, поэтому формат может использоваться и для преобразования типов выводимых объектов.

Если признака модификации (%) нет, то вся информация выводится как комментарии.

Основные модификаторы формата:

%d (%i)

– десятичное целое число;

%c

– один символ;

%s

– строка символов;

%f

– число с плавающей точкой, десятичная запись;

%е

– число с плавающей точкой, экспоненциальная запись;

%g

– используется вместо f, e для исключения незначащих нулей;

%o

– восьмеричное число без знака;

%x

– шестнадцатеричное число без знака.

Для чисел long добавляется символ l, например, %ld – длинное целое, %lf – число вещественное с удвоенной точностью – double.

Если нужно напечатать сам символ %, то его нужно указать 2 раза:

printf ("Только %d%% предприятий не работало. \n",5);

Получим:  Только 5% предприятий не работало.

Управляют выводом специальные последовательности символов: \n – новая строка; \t – горизонтальная табуляция; \b – шаг назад; \r – возврат каретки; \v – вертикальная табуляция;  \\ – обратная косая; \' – апостроф; \" – кавычки; \0 – нулевой символ (пусто).

Пример:

#define  PI   3.14159

. . .

int number = 5;

float bat = 255;

int cost = 11000;

. . .

printf(" %d студентов съели %f бутербродов. \n", number, but);

printf(" Значение числа pi равно %f. \n", pi);

printf(" Стоимость этой вещи %d %s. \n", cost, "Руб.");

. . .

В модификаторах формата функции printf после символа % можно указывать число, задающее минимальную ширину поля вывода, например, %5d – для целых,  %4.2f  –  для вещественных – две цифры после запятой для поля шириной 4 символа. Если указанных позиций для вывода целой части числа не хватает, то происходит автоматическое расширение.

Если после «%» указан знак «минус», то выводимое значение будет печататься с левой позиции поля вывода, заданной ширины, например: % – 10d.

Использование функции printf для преобразования данных:

1) printf("%d", 336.65);  получим:  336;

2) printf("%o", 336); получим:  520, т.е.  5*8**2+2*8+0*1 = 336;

3) printf("%x", 336);  получим:  150  (шестнадцатеричное).

Можно использовать функцию printf для нахождения кода ASCII некоторого символа:

printf (" %c  –  %d\n", 'a', 'a');

получим десятичный код ASCII символа а:     a  –  65 .

Функция puts(ID строки); выводит на экран дисплея строку символов, автоматически добавляя к ней символ перехода на начало новой строки (\n).

Аналогом такой функции будет: printf(“%s \n”, ID строки);

Функция putchar() выдает на экран дисплея один символ без добавления символа ‘\n’.

5.4. Функции ввода информации 

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

scanf (управляющая строка , список адресов объектов ввода);

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

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

Если нужно ввести значение строковой переменной, то использовать символ & не нужно, т.к. строка – это  массив символов, а ID массива является адресом его первого элемента. Например:

     int course;

     double grant;

     char name[20];

     printf (" Укажите курс, стипендию, имя \n ");

     scanf ("%d %lf %s", &course, &grant,  name);

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

Функция scanf() использует практически тот же набор модификаторов форматов, что и printf(); отличия от функции вывода следующие: отсутствует формат %g, форматы  %e,%f – эквивалентны. Для ввода коротких целых чисел введен модификатор формата %h.

Внимание. Функцией scanf() по формату %s строка вводится только до первого пробела.

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

 gets (ID строковой переменной);

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

Если же использовать ее в правой части операции присваивания, например:

char c;

   . . .

c = getch();

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

С началом работы любой программы автоматически открываются стандартные потоки для ввода (stdin) и вывода данных (stdout), которые по умолчанию связаны с клавиатурой и экраном монитора соответственно.

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

fflush(stdin);   

__________________________________________________________________

В языке С++ существует наиболее простая с точки зрения использования возможность ввода-вывода  потоковый ввод-вывод, основы которого рассмотрены в разд. 16.1, 16.2.

__________________________________________________________________

Советы по программированию

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

  1.  Выбирайте тип для переменных с учетом диапазона их возможных значений и требуемой точности представления данных.

Старайтесь давать переменным ID (имена), отражающие их назначение.

  1.  При вводе данных с клавиатуры выводите на экран пояснения: что нужно ввести, т.е. организуйте диалог. Для контроля сразу же после ввода выводите исходные данные на дисплей (хотя бы  в процессе отладки).
  2.  До запуска программы подготовьте тестовые примеры, содержащие исходные данные и ожидаемые результаты. Отдельно нужно проверить реакцию программы на заведомо неверные исходные данные. Для таких ситуаций необходимо предусмотреть вывод сообщений, например, «Ошибка! Решения нет».

При составлении выражений учитывайте приоритет используемых операций.

  1.  В функциях ввода/вывода printf и scanf для каждой переменной указывайте спецификацию формата, соответствующую ее типу. Не забывайте, что в функции scanf передается адрес переменной, а не ее значение.
  2.  При использовании стандартных функций требуется с помощью директивы препроцессору include подключить к программе соответствующие заголовочные файлы. Установить, какой именно файл необходим, можно с помощью справочной системы Visual C++ 6.0 – «MSDN».

8. Данные при вводе разделяйте пробелами, символами перевода строки или табуляции, но не запятыми.

__________________________________________________________________

Не смешивайте в одной программе ввод/вывод с помощью стандартных функций (в стиле Си) с вводом/выводом в потоке (в стиле C++).

__________________________________________________________________

ЗАДАНИЕ 1. Составление линейных алгоритмов

Первый уровень сложности

Составить программу для расчета двух значений z1 и z2, результаты которых должны совпадать [32]. Ввод исходных данных можно задавать при декларации или вводить с клавиатуры. Игнорировать возможность деления на ноль. Значение = 3,1415926.

1. .

2. .

3.  .

4. .

5.  .

6.   .

7.  .

8. .

9.   .

10. .

11.  .

12.  .

13.  .

14.  .

15. .

Второй уровень сложности

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

1. .

При x = 14.26, y = –1.22, z = 3.510-2, результат t = 0.564849.

2.  .

При x = –4.5, y = 0.7510-4, z = 0.845102, результат u = –55.6848.

3.  .

При x = 3.7410-2, y = –0.825, z = 0.16102, результат  v = 1.0553.

4.  .

При x = 0.4104, y = –0.875, z = –0.47510-3,  результат w = 1.9873.

5.

При x = –15.246, y = 4.64210-2, z = 20.001102,  результат = –182.036.

6.  .

При x = 16.5510-3, y = –2.75, z = 0.15, результат    = –38.902.

7.

При x = 0.1722, y = 6.33, z = 3.2510-4,  результат = –172.025.

8.

При x = –2.23510-2, y = 2.23, z = 15.221, результат = 39.374.

9.

При x  =  1.825102, y = 18.225, z = –3.29810-2,  результат = 1.2131.

10.

При x = 3.98110-2, y = –1.625103, z = 0.512, результат a = 1.26185.

11.

При x = 6.251, y = 0.827, z = 25.001, результат b = 0.7121.

12.

При x = 3.251, y = 0.325, z = 0.46610-4,  результат c = 4.025.

13. .

При x = 17.421, y = 10.36510-3, z = 0.828105,  результат f = 0.33056.

14. .

При x = 12.310-1, y = 15.4, z = 0.252103,  результат g = 82.8257.

15. .

При x = 2.444, y = 0.86910-2, z = –0.13103,  результат h = –0.49871.


ГЛАВА 6. Составление разветвляющихся алгоритмов

6.1. Краткая характеристика операторов языка Си

Операторы языка Си можно разделить на три группы: операторы-декларации (рассмотрены ранее), операторы преобразования объектов и операторы управления процессом выполнения алгоритма.

Программирование процесса преобразования объектов производится посредством записи операторов (инструкций).

Простейший вид операторов – выражение, заканчивающееся символом «;» (точка с запятой).  Выполнение такого оператора заключается в вычислении некоторого выражения.

Простые операторы: оператор присваивания (выполнение операций присваивания), оператор вызова функции (выполнение операции вызова функции), пустой оператор «;» – частный случай выражения. Пустой оператор используют тогда, когда по синтаксису оператор требуется, а по смыслу – нет (например, смотри бесконечный оператор цикла for в разд. 7.4).

Примеры операторов «выражение»:

i++; – выполняется операция инкремента (увеличение на 1);

x+y; – выполняется операция сложения (результат будет утерян);

a = bc; – выполняется операция вычитания с одновременным присваиванием.

Операторы языка Си записываются в свободном формате с использованием разделителей между ключевыми словами. Любой оператор может помечаться меткой – идентификатор и символ «:» (двоеточие).  Область действия метки – функция, где эта метка определена.

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

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

Допускается вложенность операторов. В случае необходимости можно использовать составной оператор – блок, состоящий из любой последовательности операторов, заключенных в фигурные скобки – { и }, после закрывающей скобки символ «;» не ставится.

6.2. Условные операторы

Условный оператор if используется для разветвления процесса выполнения кода программы на два направления.

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

       if (выражение) оператор;

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

Структурная схема простого оператора приведена на рис. 6.1.

Примеры записи условного оператора if:

if (x > 0) x = 0;

if (i != 1)   j++,  s = 1;  – используем операцию «запятая»;

if (i != 1) {

j++;  s = 1;  – последовательность операций (блок);

}  

if (getch() != 27) k = 0; – если нажата любая клавиша кроме “Esc”.

if (!x) exit (1);   или    if (x == 0) exit(1);

if (i>0 && i<n) k++;  – если нужно проверить несколько условий, то их объединяют знаками логических операций и заключают в круглые скобки (для улучшения читаемости программы можно ставить круглые скобки и там где они необязательны);

     if (a++) b++;   – необязательно в качестве выражения использовать логические выражения.

Синтаксис полного оператора условного выполнения:

       if (выражение) оператор 1 ;

              else  оператор 2 ;

Если выражение не равно нулю (истина), то выполняется  оператор 1, иначе – оператор 2. Операторы 1 и 2 могут быть простыми или составными (блоками).

Наличие символа «;» перед словом else в языке Си обязательно.

Структурная схема такого оператора приведена на рис. 6.2.

Примеры записи:

        if (x > 0) j = k+10;

         else m = i+10;

          if ( x>0 &&  k!=0 ) {

j = x/k;

x += 10;

}

        else m = k*i + 10;

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

      if (n > 0)

              if(a > b)  z = a;

           else   z = b;

Здесь ветвь else связана со вторым if (a > b). Если же необходимо связать фразу  else с внешним if, то используются операторные скобки:     

   if(n > 0) {

if(a > b)  z = a;

}

  else   z = b;

В следующей цепочке операторов if – else – if выражения просматриваются последовательно:

      if (выражение 1)  оператор 1;

          else

if (выражение 2)  оператор 2;

           else

if (выражение 3)  оператор 3;

           else   оператор 4 ;

Если какое-то выражение оказывается истинным, то выполняется относящийся к нему оператор и этим вся цепочка заканчивается. Каждый оператор может быть либо отдельным оператором,  либо группой операторов в фигурных скобках. Оператор 4 будет выполняться только тогда, когда ни одно из проверяемых условий не подходит. Иногда при этом не нужно предпринимать никаких явных действий, тогда последний else может быть опущен или его можно использовать для контроля, чтобы зафиксировать «невозможное» условие (своеобразная экономия на проверке условий).

Пример:

if (x < 0) printf("\n X отрицательное \n");

       else if(x==0) printf ("\n X равно нулю \n");

        else printf("\n X положительное \n");

Замечание. Наиболее распространенной ошибкой при создании условных операторов является использование в выражении операции присваивания «=» вместо операции сравнения на равенство операндов «==» (два знака равно). Например, в следующем операторе синтаксической ошибки нет:

if (x = 5) a++; 

но значение а будет увеличено на единицу независимо от значения переменной х, т.к. результатом операции присваивания х = 5 в круглых скобках является значение 50 – истина.  

6.3. Условная операция «? :»

Условная операция – тернарная, т.к. в ней участвуют три операнда. Формат написания условной операции следующий:

 Выражение 1 ? выражение 2 : выражение 3;

если выражение 1 (условие) отлично от нуля (истинно), то результатом операции является значение выражения 2, в противном случае – значение выражения 3. Каждый раз вычисляется  только  одно  из выражений 2 или 3.

На рис. 6.3 приведена схема вычисления результата, которая аналогична схеме полного оператора if (см. рис. 6.2):

   

Условное вычисление применимо к арифметическим операндам и операндам-указателям.

Рассмотрим участок программы для нахождения максимального значения z из двух чисел a и b, используя оператор if и условную операцию.

1. Запишем оператор if :

      if (a > b)  z = a;

             else  z = b;

2. Используя условную операцию, получим

      z = (a > b) ? a : b;

Условную операцию можно использовать так же, как и любое другое выражение. Если выражения 2 и 3 имеют разные типы, то тип результата определяется по правилам преобразования. Например, если f имеет тип double, а nint, то результатом операции

       (n > 0) ? f : n;

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

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

int a, x;

    ...

x = (a < 0) ? –a : a;

printf("\n Значение %d  %s нулевое !", x, (x ? "не" : " ") );

6.4. Оператор выбора альтернатив (переключатель)

Оператор switch (переключатель) предназначен для разветвления процесса вычислений на несколько направлений.

Общий вид оператора:

switch ( выражение ) {

case  константа1:  список операторов 1

case  константа2:  список операторов 2

  ...

case  константаN:  список операторов N

default:  список операторов N+1     – необязательная ветвь;

}

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

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

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

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

Управляющий оператор break (разрыв) выполняет выход из оператора switch. Если по совпадению с каждой константой должна быть выполнена одна и только одна ветвь, схема оператора switch следующая:

switch (выражение) {

         case константа1: операторы 1;  break;     

         case константа2: операторы 2;  break;

   ...      

  case константаN: операторы N;  break;

             default:  операторы (N+1);  break;    

  }

Структурная схема рассмотренной конструкции (с использованием оператора break) приведена на рис. 6.4.

Пример оператора switch с использованием оператора break:

     void main(void)   {   

int i = 2;

      switch(i)       {

         case 1:  puts ( "Случай 1. "); break;

         case 2:  puts ( "Случай 2. "); break;

         case 3:  puts ( "Случай 3. "); break;

         default: puts ( "Случай default. "); break;

         }

      }

Результатом выполнения данной программы будет:

Случай 2.

Аналогичный пример без использования оператора break (схема общего вида такой конструкции приведена рис. 6.5):

void main() {   

int i = 2;

      switch(i)       {

         case 1: puts ( "Случай 1. ");

         case 2: puts ( "Случай 2. ");

         case 3: puts ( "Случай 3. ");

         default: puts ( "Случай default. ");

         }

      }

В данном случае результат будет следующим:

Случай 2.

Случай 3.

Случай default.

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

Текст программы может быть следующим:

#include <stdio.h>

void main(void)

{

double a, b, c;

char s;

m1:  fflush(stdin);   // Очистка буфера ввода stdin

printf("\n Введите операнд 1, символ операции, операнд 2:");

scanf("%lf%c%lf", &a, &s, &b);

switch(s)  {

case '+':  c = a+b; break;

case '–':  c = a–b;   break;

case '*':  c = a*b;   break;

case '/':  c = a/b;     break;

default: printf("\n Ошибка, повторите ввод! "); goto m1;

      }

 printf("\n a %c b = %lf ", s, c);

 printf("\n Продолжим? (Y/y) ");

 s = getch();

 if ( (s=='Y') || (s=='y') ) goto m1;

 printf("\n Good bye! ");

}

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

Введите операнд 1, символ операции, операнд 2: 2.4+3.6

На экран будет выведен результат и дальнейший диалог:

a + b = 6.000000

Продолжим? (Y/y)

Введя  символ y (Y), вернемся в начало функции и на экране вновь появится:

Введите операнд 1, символ операции, операнд 2:  

Если ошибочно ввести – 2r3  , появятся следующие сообщения:

Ошибка, повторите ввод!

Введите операнд 1, символ операции, операнд 2: 2 * 3

a*b = 6.000000

Continue? (Y/y)

Нажимаем любую клавишу, кроме y или Y – следует сообщение

Good bye!

Программа закончена.


ГЛАВА 7. Составление циклических алгоритмов

7.1. Понятие циклического кода

Практически все алгоритмы решения задач содержат циклически повторяемые участки. Цикл – это одно из фундаментальных понятий программирования. Под циклом понимается организованное повторение некоторой последовательности операторов.

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

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

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

– оператор цикла с предусловием;

– оператор цикла с постусловием;

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

7.2. Оператор с предусловием while 

Цикл с предусловием реализует структурную схему, приведенную на рис. 7.1, а, и имеет вид

while (выражение)

код цикла;

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

Если выражение в скобках – истина (не равно 0), то выполняется код цикла. Это повторяется до тех пор, пока выражение не примет значение 0 (ложь).  В  этом случае происходит выход из цикла и выполняется оператор, следующий за конструкцией while. Если выражение в скобках изначально ложно (т.е. равно 0), то цикл не выполнится ни разу.

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

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

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

Рис. 7.1. Схемы операторов цикла:

а – цикл с предусловием; б – цикл с постусловием

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

Для этого используют оператор continue – переход к следующей итерации цикла и break – выход из цикла (см. разд. 9.2, 9.3).

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

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

         int count = 0;

         char ch = getchar();

         while ( ch != \n') {

                count++;

                ch = getchar();

          }

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

             char ch;

             int count = 0;

             while (( ch=getchar()) != '\n') count++;

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

           int count = 0;

           while ( getchar() !='\n') count ++;

Полезные примеры

1. Организация выхода из бесконечного цикла по нажатии клавиши Esc

while (1) {     // Бесконечный цикл 

       ...

      if (kbhit() && getch()==27 ) break;  

       ...

}

Функция kbhit() возвращает значение > 0, если нажата любая клавиша, а функция  getch() возвращает код нажатой клавиши (код  клавиши Esc равен 27).  В результате выполнения оператора if, если будет нажата клавиша Esc, выполнится оператор break и произойдет выход из цикла.

Приведенный пример – распространенный прием программирования.

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

 ...   

while (!kbhit());   

  ...   

7.3. Оператор цикла с постусловием dowhile

Цикл с постусловием реализует структурную схему, приведенную на рис. 7.1, б.

Общий вид записи такой конструкции

do  

код цикла;  

while (выражение);

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

Здесь сначала выполняется код цикла, после чего проверяется, надо ли его выполнять еще раз.

Следующая программа будет «вас приветствовать» до тех пор, пока будем вводить символ Y или y (Yes). После введения любого другого символа цикл завершит свою работу.

#include <stdio.h>

void main(void)

{

char answer;

do {

puts(" Hello! => ");

scanf(" %c ", &answer);

}

while ((answer=='y')||(answer=='Y'));

}

Результат выполнения программы:

Hello! => Y

Hello! => y

Hello! => d

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

Общий вид оператора:

          for (выражение 1; выражение 2; выражение 3)

код цикла;

где  выражение 1 – инициализация счетчика (параметр цикла);

выражение 2 – условие продолжения счета;

выражение 3 – коррекция  счетчика.

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

Рис. 7.2. Схемы оператора цикла for:
а –
схема работы; б – блок-схема  

Инициализация используется для присвоения счетчику (параметру цикла) начального значения.

Выражение 2 определяет условие выполнения цикла. Как и в предыдущих случаях, если его результат не нулевой («истина»), – то цикл выполняется, иначе – происходит выход из цикла.

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

Выражения 1, 2 и 3 могут отсутствовать (пустые выражения), но символы «;» опускать нельзя.

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

sum = 0;

           for ( i = 1; i<=N; i++)  sum+=i;

Заметим, что в выражении 1 переменную-счетчик можно декларировать. Например:

for (int i = 1; i<=N; i++)

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

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

Цикл for эквивалентен последовательности инструкций:

          выражение 1;

          while (выражение 2) {

...

                выражение 3;

         }

а оператор          for (; выражение 2; )

код цикла;

эквивалентен оператору     while (выражение 2)

код цикла;

Если пропущено выражение 2, то цикл будет выполняться бесконечно, поскольку пустое условие всегда остается истинным. Бесконечный оператор:     for ( ; ; ) код цикла;

эквивалентен оператору while (1) код цикла;

В заголовке оператора for может использоваться операция «запятая». Она позволяет включать в его выражения несколько операторов. Тогда рассмотренный пример суммирования первых N натуральных чисел можно записать в следующем виде:

         for ( sum = 0 , i = 1; i<=N; sum+= i , i++) ;

Оператор for имеет следующие возможности:

– можно вести подсчет с помощью символов, а не только чисел:

          for (ch = 'a'; ch <= 'z'; ch++) ... ;

– можно проверить выполнение некоторого произвольного условия:

         for (n = 0; s[i] >= '0' && s[i] < '9'; i++) ... ;

или 

         for (n = 1; n*n*n <= 216; n++) ... ;

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

         for (printf(" вводить числа по порядку! \n"); num!=6;)

           scanf("%d", & num);

         printf(" последнее число – это то, что нужно. \n");

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

Переменные, входящие в выражения 2 и 3, можно изменять при выполнении кода цикла, например, значения k и delta:

         for (n = 1; n < 10*k; n += delta) ... ;

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

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

      printf("%6d%c", a[i],( (i%10==0) || (i==n–1) ) ? '\n' : ′ ′);

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

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

Чтобы избежать ошибок, нужно стараться:

– проверить, всем ли переменным, встречающимся в правой части операторов присваивания в коде цикла, присвоены до этого начальные значения (а также возможно ли выполнение других операторов);

– проверить, изменяется ли в цикле хотя бы одна переменная, входящая в условие выхода из цикла;

– предусмотреть аварийный выход из цикла по достижении некоторого количества итераций;

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


ГЛАВА 8. Операторы и функции передачи управления

Формально к операторам передачи управления относятся:

– оператор безусловного перехода  goto;

– оператор перехода к следующему шагу (итерации) цикла continue;

– выход из цикла, либо оператора switchbreak;

– оператор возврата из функции return.

8.1. Оператор безусловного перехода goto 

В языке Си предусмотрен оператор goto, общий вид которого

           goto  метка ;

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

m1:    ;   

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

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

          for (...)

             for (...) {

...

                   if (ошибка)  goto error;

               }

                  ...

                 error:  операторы для устранения ошибки;

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

8.2. Операторы continue, break и return

Оператор continue может использоваться во всех типах циклов (но не в операторе-переключателе switch). Наличие оператора continue вызывает пропуск «оставшейся» части итерации и переход к началу следующей, т.е. досрочное завершение текущего шага и переход к следующему шагу.

В циклах while и do-while это означает непосредственный переход к проверочной части. В цикле for управление передается на шаг коррекции, т.е. модификации выражения 3.

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

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

Оператор return производит досрочный выход из текущей функции. Он также возвращает значение результата функции:

return  выражение;

Выражение должно иметь скалярный тип. Правила использования данного оператора будут рассмотрены в гл. 12.

8.3. Функции exit и abort

Функция exit выполняет прерывание программы и используется для нормального, корректного завершения работы программы при возникновении какой-либо внештатной ситуации, например, ошибка при открытии файла (гл. 14). При этом записываются все буферы в соответствующие файлы, закрываются все потоки и вызываются все зарегистрированные стандартные функции завершения.

Прототип этой функции приведен в заголовочном файле stdlib.h и выглядит так:

void exit ( int exit_code);

Параметр данной функции – ненулевое целое число, передаваемое системе программирования (служебное сообщение о возникшей внештатной ситуации).

Для завершения работы программы также может использоваться функция

void abort (void);

действия которой аналогичны функции exit(3).

Советы по программированию

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

1. Выражение, стоящее в круглых скобках операторов if, while и do-while, вычисляется по правилам стандартных приоритетов операций.

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

3. Проверка вещественных величин на равенство, как правило, из-за ограниченной разрядности дает неверный результат.

4. Чтобы получить максимальную читаемость и простоту структуры программы, надо правильно выбирать способ реализации ветвлений (с помощью if, switch, или условных операций), а также наиболее подходящий оператор цикла.

5. Выражение в операторе switch и константные выражения в case должны быть целочисленного или символьного типов.

6. Рекомендуется использовать  в операторе switch ветвь default.

7. После каждой ветви для передачи управления на точку кода за оператором switch используется оператор break.

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

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

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

ЗАДАНИЕ 2. Разветвляющиеся алгоритмы

Первый уровень сложности

Составить программу нахождения требуемого значения с исходными данными x, y, z. Обозначение: min и max – нахождение минимального и максимального из перечисленных в скобках значений элементов.

1.  ;  2. ;

3. ; 4. ;

5. ;  6. ;

7. ;  8. ;

9. ; 10. ;

11. ; 12. ;

13. ;  14. ;  

15. .

Второй уровень сложности

Вычислить значение y в зависимости от выбранной функции (x), аргумент которой определяется из поставленного условия. Возможные значения функции (x): 2x, x2, х/3. Предусмотреть вывод сообщений, показывающих, при каком условии и с какой функцией производились вычисления у.

  1.  ,   где  
  2.  ,   где  
  3.  ,  где
  4.  ,   где
  5.  , где  
  6.  ,   где  
  7.  ,   где
  8.  , где
  9.  ,   где
  10.  ,   где  
  11.  ,   где
  12.  , где
  13.  ,  где
  14.  ,  где
  15.  ,  где

ЗАДАНИЕ 3. Циклические алгоритмы

Первый уровень сложности

Составить программу для определения таблицы значений функции у в произвольном диапазоне [a, b] изменения аргумента х с произвольным шагом h. Значения a, b, h вводятся с клавиатуры. Таблица должна содержать следующие столбцы: порядковый номер, значение аргумента x, значение функции, сообщение о возрастании или убывании функции.

Определить максимальное и минимальное значения функции.

1.        a = –; b = ; h = 0,4.

2.       a = 0,7; b = 1,8; h = 0,1.

3.     a = –0,5; b = 2,5; h = 0,2.

4.    a = –0,9; b = 2,7; h = 0,3.

5.     a = –2; b = 0,8; h = 0,2.

6.     a = –1,9; b = 2,7; h = 0,3.

7.     a = –0,4; b = 0,4; h = 0,5.

8.       a = –0,3; b = 1,3; h =  /10.

9.     a = –/2; b =  /2; h = /10.

10.     a = –3; b = 3; h = 0,5.

11.      a = –; b = ; h =  /6.

12.    a = –0,9; b = 1, h = 0,3.

13.    a = –0,9; b = 2,7; h = 0,3.

14.      a = –0,1; b = 2; h = 0,1.

15.      a = ; b = 2; h =  /15.

Второй уровень сложности

Значение аргумента x изменяется от a до b с шагом h. Для каждого x найти значения функции Y(x), суммы S(x) и |Y(x)–S(x)| и вывести в виде таблицы. Значения a, b, h и n вводятся с клавиатуры. Так как значение S(x) является рядом разложения функции Y(x), значения S и Y для заданного аргумента x должны совпадать в целой части и в первых двух-четырех позициях после десятичной точки.

Работу программы проверить для a = 0,1; b = 1,0; h = 0,1; значение параметра n выбрать в зависимости от задания.

1. ,  .

2.   .

3.     .

4.     .

5.     .

6.     .

7.    .

8.  ,  .

9. ,  .

10.  ,   .

11. ,  .

12. , .

13. ,  .

14. ,   .

15. ,  .

 

ГЛАВА 9. Указатели

9.1. Определение указателей

При обработке декларации любой переменной, например double x=1.5; компилятор выделяет для переменной участок памяти, размер которого определяется ее типом (double – 8 байт), и инициализирует его указанным значением (если таковое имеется). Далее все обращения в программе к переменной по имени заменяются компилятором на адрес участка памяти, в котором будет храниться значение этой переменной. Разработчик программы на языке Си  имеет возможность определить собственные переменные для хранения адресов участков оперативной памяти. Такие переменные называются указателями.

Итак, указатель – это переменная, которая может содержать адрес некоторого объекта. Простейшая декларация указателя имеет формат

тип  * ID_указателя;

Например:  int *a;  double *f; char *w;

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

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

Например, в декларации:

 int *a, *b, с;

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

Значение указателя равно первому байту участка памяти, на который он ссылается.

Указатели предназначены для хранения адресов областей памяти. В языке Cи имеются три вида указателей – указатели на объект известного типа, указатель типа void и указатель на функцию. Эти три вида различаются  как своими свойствами, так и набором допустимых операций. Указатель не является самостоятельным типом данных, так как всегда связан с каким-либо конкретным типом, т.е. указатель на объект содержит адрес области памяти, в которой хранятся данные определенного типа.

Указатель типа void применяется в тех случаях, когда конкретный тип объекта, адрес которого требуется хранить, не определен (например, если в одной и той же переменной в разные моменты времени требуется хранить адреса объектов различных типов).

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

Указатель может быть константой или переменной, а также указывать на константу или переменную.

С указателями-переменными связаны две унарные операции & и *.

Операция & означает «взять адрес» операнда. Операция * имеет смысл – «значение, расположенное по указанному адресу» (операция разадресации).

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

– по имени (идентификатору);

– по указателю (операция косвенной адресации):

      ID_указателя=&ID_объекта; – операция разыменования;

*ID_указателя   – операция косвенной адресации.

Говорят, что использование указателя означает отказ от именования  адресуемого им объекта.

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

Унарная операция получения адреса & применима к переменным, имеющим имя (ID), для которых выделены участки оперативной памяти. Таким образом, нельзя получить адрес скалярного выражения, неименованной константы или регистровой переменной (типа register).

Отказ от именования объектов при наличии возможности доступа  по указателю приближает язык Си по гибкости отображения «объект – память» к языку ассемблера.

Пример 1:

int x,   – переменная типа int ;

*y;   – указатель на объект типа int;

y = &x;  – y – адрес переменной x;

*y=1;  – косвенная адресация указателем поля x, т.е. по

  указанному адресу записать 1: x = 1.

Пример 2:

int i, j = 8, k = 5, *y;

y=&i;

  *y=2;     –  i = 2  

   y=&j;

  *y+=i;    –  j += i    j = j+i    j = j + 2 = 10

   y=&k;

   k+=*y; –  k += k k = k + k = 10

   (*y)++;   –  k++    k = k + 1 = 10 + 1 = 11  

Как видно из приведенных примеров, конструкцию *ID_указателя можно использовать в левой части оператора присваивания, так как она является L-значением (см. разд. 4.3), т.е. определяет адрес участка памяти. Эту конструкцию часто считают именем переменной, на которую ссылается указатель. С ней допустимы все действия, определенные для величин соответствующего типа (если указатель инициализирован).

Пример 3:

int i1; – целая переменная;

const int i2=1; – целая константа;

int * pi1; – указатель на целую переменную;

const int * pi2; – указатель на целую константу;

int * const pi1=&i1; – указатель-константа на целую переменную;

const int * const pi2=&i2; – указатель-константа на целую константу.

Как видно из примеров, модификатор const, находящийся между ID указателя и символом «звездочка», относится к самому указателю и запрещает его изменение, a const слева от звездочки задает константное значение объекта, на который он указывает. Для инициализации указателей использована операция получения адреса &.

Указатель подчиняется общим правилам определения области действия, видимости и времени жизни.

9.2. Операция sizeof

Формат записи:

 sizeof ( параметр );    

параметр – тип или идентификатор объекта (но не ID функции).

Данная операция позволяет определить размер указанного параметра в байтах (тип результата int).

Если указан идентификатор сложного объекта (массив, структура, объединение), то результатом будет размер всего объекта. Например:

sizeof(int) – результат  2(4) байта;

 double b[5];

sizeof(b) – результат 8 байт * 5 = 40 байт.

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

9.3. Инициализация указателей

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

Инициализатор записывается после ID указателя либо в круглых скобках, либо после знака равенства.

Существуют следующие способы инициализации указателя:

1. Присваивание указателю адреса существующего объекта:

а) используя операцию получения адреса переменной:

 int a = 5;  

int *p = &а;        – указателю p присвоили адрес объекта а;

int *p(&а);          – то же самое другим способом;

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

int *g = р;

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

      int i,*x;

      char *y;

     x = &i;                // x – поле объекта int;

      y = (char *)x;             // y – поле объекта char;

      y = (char *)&i;          // y – поле объекта char;

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

int x[100], *y;

y = x;         – присваивание константы переменной;

x = y;    – ошибка, т.к. в левой части указатель-константа.

2. Присваивание пустого значения:

int *x1 = NULL;

int *x2 = 0;

В первой строке используется константа NULL, определенная как указатель, равный нулю. Рекомендуется использовать просто цифру 0, так как это значение типа int будет правильно преобразовано стандартными способами в соответствии с контекстом. А так как объекта с нулевым (фиктивным) адресом не существует, пустой указатель обычно используют для контроля, ссылается указатель на конкретный объект или нет.

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

а) c помощью операции new (см. разд. 16.4):

int *n = new int;

int *m = new int (10);

б) c помощью функции malloc (см. разд. 10.9):

int *p = (int*)malloc(sizeof(int));

Присваивание без явного приведения типов допускается в двух случаях:

– указателям типа void*;

– если тип указателей справа и слева от операции присваивания один и тот же.

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

9.4. Операции над указателями

Помимо уже рассмотренных операций, с указателями можно выполнять арифметические операции сложения, инкремента (++), вычитания, декремента (--) и операции сравнения.

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

Инкремент перемещает указатель к следующему элементу массива, декремент – к предыдущему.

Указатель, таким образом, может использоваться в выражениях вида

       p # iv,    ## p,    p ##,    p # = iv,

p – указатель, iv – целочисленное выражение, # – символ операции '+' или '–'.

Результатом таких выражений является увеличенное или уменьшенное значение указателя на величину iv * sizeof(*p), т.е. если указатель на определенный тип увеличивается или уменьшается на константу, его значение изменяется на величину этой константы, умноженную на размер объекта данного типа.

Текущее значение указателя всегда ссылается на позицию некоторого объекта в памяти с учетом правил выравнивания для соответствующего типа данных. Таким образом, значение p # iv указывает на объект того же типа, расположенный в памяти со смещением на iv позиций.

При сравнении указателей могут использоваться отношения любого вида («>», «<» и т.д.), но наиболее важными видами проверок являются отношения равенства и неравенства («==», «!=»).

Отношения порядка имеют смысл только для указателей на последовательно размещенные объекты (элементы одного массива).

Разность двух указателей дает число объектов адресуемого ими типа в  соответствующем диапазоне адресов, т.е. в применении к массивам разность указателей, например, на третий и шестой элементы равна 3.

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

Значение указателя можно вывести на экран с помощью функции printf, используя спецификацию %p (pointer), результат выводится в шестнадцатеричном виде.

Рассмотрим фрагмент программы:

int a = 5, *p, *p1, *p2;

p = &a;  

p2 = p1 = p;

++p1;

p2 += 2;

printf(“a = %d , p = %d , p = %p , p1 = %p , p2 = %p .\n”, a, *p, p, p1, p2);

Результат может быть следующим:

a = 5 , *p = 5 , p = FFF4 , p1 = FFF6, p2 = FFF8 .

Графически это выглядит следующим образом (в 16-разрядном процессоре на тип int отводится 2 байта):

FFF5

FFF7

FFF9

FFF4

р

FFF6

p1

FFF8

p2

FFF10

p   = FFF4,   

p1 = FFF6 = ( FFF4 + 1*sizeof(*p)) FFF4 + 2 (int)

р2 = FFF8 = ( FFF4 + 2*sizeof(*p)) FFF4 + 2*2

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

При смешивании в выражении указателей разных типов явное преобразование типов требуется для всех указателей, кроме void*.

Явное приведение типов указателей позволяет получить адрес объекта любого типа:

    type *p;

    p = (type*) &object;

Значение указателя p позволяет работать с переменной object как объектом  типа  type.

ГЛАВА 10. Массивы

10.1. Понятие массива

В математике для удобства записи различных операций часто используют индексированные переменные: векторы, матрицы и т.п. Так, вектор  представляется набором чисел (c1, c2, ..., cn), называемых его компонентами, причем каждая компонента имеет свой номер, который принято обозначать в виде индекса. Матрица А – это таблица чисел (аij, i=1,..., n; j=1,..., m), i – номер строки, j – номер столбца. Операции над матрицами и векторами обычно имеют короткую запись, которая обозначает определенные, порой сложные действия над их индексными компонентами. Например, произведение двух векторов записывается как . Произведение матрицы на вектор .

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

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

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

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

Описание массива в программе отличается от описания простой переменной наличием после имени квадратных скобок, в которых задается количество элементов массива. Например, double a [10];  – описание массива из 10 вещественных чисел.

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

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

10.2. Одномерные массивы

В программе одномерный массив объявляется следующим образом:

тип   ID_массива [размер] = {список начальных значений};

тип – базовый тип элементов массива (целый, вещественный, символьный); размер – количество элементов в массиве.

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

При декларации массива можно использовать также атрибуты «класс памяти» и const.

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

Пример объявления массива целого типа: int a[5];

Индексы массивов в языке Си начинаются с 0, т.е. в массиве а первый элемент: а[0], второй – а[1], … пятый – а[4].

Обращение к элементу массива в программе на языке Си осуществляется в традиционном для многих других языков стиле – записи операции обращения по индексу [] (квадратные скобки), например:

a[0]=1;           

a[i]++;            

a[3]=a[i]+a[i+1];

Пример объявления массива целого типа с инициализацией начальных значений:

int a[5]={2, 4, 6, 8, 10};

Если в группе {…} список значений короче, то оставшимся элементам присваивается 0.

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

10.3. Связь указателей и массивов

Идентификатор одномерного массива – это адрес памяти, начиная с которого он расположен, т.е. адрес его первого элемента. Таким образом, работа с массивами тесно взаимосвязана с применением указателей. Рассмотрим связь указателей с элементами одномерного массива на примере.

Пусть объявлены одномерный целочисленный массив a из 5  элементов и указатель p на целочисленные переменные:  

int a[5]={1, 2, 3, 4, 5}, *p;

ID массива a является константным указателем на его начало, т.е. а = &a[0] – адрес начала массива.  Расположение массива а в оперативной памяти, выделенной компилятором, может выглядеть следующим образом:

a[0]

a[1]

a[2]

a[3]

a[4]

– элементы массива;

1

2

3

4

5

– значения элементов массива;

4000

4002

4004

4006

4008

– символические адреса.

Указатель а содержит адрес начала массива и в нашем примере равен 4000 (а = = 4000).

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

р = а;  

что эквивалентно выражению р = &a[0];  то получим, что и р = 4000. Тогда  с учетом адресной арифметики обращение к i-му элементу массива а может быть записано следующими выражениями:

а[i]     *(а+i)      *(р+i)     р[i]  ,

приводящими к одинаковому результату.

Идентификаторы а и р – указатели, очевидно, что выражения а[i] и *(а+i) эквивалентны. Отсюда следует, что операция обращения к элементу массива по индексу применима и при его именовании переменной-указателем. Таким образом, для любых указателей можно использовать две эквивалентные формы выражений для доступа к элементам массива: р[i] и *(р+i). Первая форма удобнее для читаемости текста,  вторая – эффективнее по быстродействию программы.

Например, для получения значения 4-го элемента массива можно написать а[3] или *(а+3), результат будет один и тот же, а операции a[3] = 8 и *(р+3) = 8 дадут тождественный результат, т.к. р+3 = 4000+3*sizeof(int) = = 4000+3*2 = 4006, т.е. указатель р установлен на четвертый по счету элемент массива.  

Очевидна и эквивалентность выражений:

– получение адреса начала массива в ОП:

&а[0]    &(*а)     а

– обращение к первому элементу массива:

*а    а[0]

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

int x[]={1, 2, 3, 4, 5, 6, 7};   

Размер n должен быть целочисленной константой:

int n = sizeof(x) / sizeof(*x);

10.4. Строки как одномерные массивы данных типа char

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

Нулевой байт – это байт, каждый бит которого равен нулю, при этом для нулевого байта определена символьная константа ´\0´ (признак окончания строки, или «нуль-символ»). Поэтому если строка должна содержать k символов, то в описании массива размер должен быть k+1. По положению нуль-символа определяется фактическая длина строки.

Например, char s[7]; – означает, что строка может содержать не более шести символов, а последний байт отводится под нуль-символ.

Отсутствие нуль-символа и выход указателя при просмотре строки за ее пределы – распространенная ошибка при работе со строками.

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

сhar S[ ] = “Работа со строками”;

для данной строки выделено и заполнено 19 байт – 18 на символы и 19-й на нуль-символ.

В конце строковой константы явно указывать символ ´\0´ не нужно. Компилятор добавит его автоматически.

Символ ´\0´ нужно использовать явно тогда, когда символьный массив при декларации инициализируется списком начальных значений, например, следующим образом:

char str[10] ={‘V’ , ‘a’, ‘s’, ‘j’ , ‘а’, ‘\0’};

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

При работе со строками можно пользоваться указателями, например:

char *x;

x = "БГУИР";

x = (i>0) ? "положительное" : (i<0) ? "отрицательное" : "нулевое";

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

Операция char *str = "БГУИР" создает не строковую переменную, а указатель на строковую константу, изменить которую невозможно, причем это касается не только адреса ОП, но и его размера. Знак равенства перед строковым литералом означает инициализацию, а не присваивание.

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

Процесс копирования строки s1 в строку s2 имеет вид

char s1[25], s2[25];

for (int i = 0; i <= strlen(s1); i++)

s2[i] = s1[i];

Длина строки определяется с помощью стандартной функции strlen, которая вычисляет длину, выполняя поиск нуль-символа (прототип функции приведен ниже). Таким образом, строка фактически просматривается дважды.

А вот следующие действия будут ошибочными:

сhar s1[51];

s1 = ”Minsk”;

Это связано с тем, что s1 – константный указатель и не может использоваться в левой части операции присваивания.

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

strcpy(s1, ”Minsk”);

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

Функция scanf вводит значения для строковых переменных при помощи формата (спецификатора ввода) %s до появления первого символа “пробел” (символ «&» перед ID строковых данных указывать не надо);

Функция gets осуществляет ввод строки, которая может содержать  пробелы. Завершается ввод нажатием клавиши Enter.

Обе функции автоматически ставят в конец строки нулевой байт.

Вывод строк производится функциями printf или puts до нулевого байта.

Функция printf не переводит курсор после вывода на начало новой строки, а puts автоматически переводит курсор после вывода строковой информации в начало новой строки. Например:

char Str[30];

printf(“ Введите строку без пробелов : \n”);

 scanf(“%s”, Str);

или

puts(“ Введите строку ”);

gets(Str);

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

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

Функция strlen(S) возвращает длину строки (количество символов в строке), при этом завершающий нулевой байт не учитывается, например:

char *S1 = ”Минск!\0”, S2[] = ”БГУИРУра!”;

printf(“ %d , %d .”, strlen(S1), strlen(S2));

Результат выполнения данного участка программы:  

6 , 10 .

Функция strcpy(S1, S2) – копирует содержимое строки S2 в строку S1.

Функция strcat(S1, S2) – присоединяет строку S2 к строке S1 и помещает ее в массив, где находилась строка S1, при этом строка S2 не изменяется. Нулевой байт, который завершал строку S1, заменяется первым символом строки S2.

Функция int strcmp(S1, S2) сравнивает строки S1 и S2 и возвращает значение <0, если S1<S2; >0, если S1>S2; =0, если строки равны, т.е. содержат одно и то же число одинаковых символов.

Функции преобразования строковых объектов в числовые описаны в библиотеке stdlib.h. Рассмотрим некоторые из них.

Преобразование строки S в число:

целое:  int atoi(S);

длинное целое:  long atol(S);

– действительное:  double atof(S);

при возникновении ошибки данные функции возвращают значение 0.

Функции преобразования числа V в строку S:

целое:  itoa(V, S, kod);

– длинное целое:  ltoa(V, S, kod);

2  kod  36, для десятичных чисел со знаком kod = 10.

Пример участка кода программы, в котором из строки s удаляется символ, значение которого содержится в переменной с каждый раз, когда он встречается

char s[81], c;

...

for( i = j = 0; s[i] != '\0'; i++)

     if( s[i] != c)  s[j++] = s[i];

s[j]='\0';

...

__________________________________________________________________

В режиме консольных приложений в среде Visual C++ 6.0 вывод символов русского языка сопряжен с определенными неудобствами. Разрешение данной проблемы рассматривается в разд. 16.3.

__________________________________________________________________

10.5. Указатели на указатели

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

Объявление массива указателей на целые числа имеет вид

int *a[10], y;

Теперь каждому из элементов массива указателей a можно присвоить адрес целочисленной переменной y, например: a[1]=&y;

Чтобы теперь найти значение переменной y через данный элемент массива а, необходимо записать *a[1].

В языке Си можно описать переменную типа «указатель на указатель». Это ячейка оперативной памяти (переменная), в которой будет храниться адрес указателя на некоторую переменную. Признак такого типа данных – повторение символа «*» перед идентификатором переменной. Количество символов «*» определяет уровень вложенности указателей друг в друга. При объявлении указателей на указатели возможна их одновременная инициализация. Например:

int a=5;    

int *p1=&a;

int **pp1=&p1;

int ***ppp1=&pp1;

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

a=10;     *p1=10; **pp1=10; ***ppp1=10;

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

*p1   ~  p1[0] ;

**pp1  ~  pp1[0][0] ;

***ppp1  ~  ppp1[0][0][0] .

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

10.6. Многомерные массивы

Декларация  многомерного массива имеет следующий формат:

тип ID[размер1][размер2]…[размерN] =

{ {список начальных значений},

       {список начальных значений},

};

Списки начальных значений – атрибут необязательный.

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

Рассмотрим особенности работы с многомерными массивами на конкретном примере двухмерного массива.

Например, пусть приведена следующая декларация двухмерного массива:  

int m[3][4];

Идентификатор двухмерного массива – это указатель на массив указателей (переменная типа указатель на указатель: int **m;).

Поэтому двухмерный массив m[3][4]; компилятор рассматривает как массив трех указателей, каждый из которых указывает на начало массива со значениями размером по четыре элемента каждый. В ОП данный массив будет расположен следующим образом:

Указа-тели  

m [0]

m[0][0]

m[0][1]

m[0][2]

m[0][3]

m [1]

m[1][0]

m[1][1]

m[1][2]

m[1][3]

m [2]

m[2][0]

m[2][1]

m[2][2]

m[2][3]

     (А)     (В)

Рис. 10.1. Схема размещения элементов массива m размером 3×4

Причем в данном случае указатель m[1] будет иметь адрес m[0]+4*sizeof(int), т.е. каждый первый элемент следующей строки располагается за последним элементом предыдущей строки.

Приведем пример программы конструирования массива массивов:

#include <stdio.h>

void main()

{

int x0[4] = { 1, 2, 3,4};      //  Декларация и инициализация  

int x1[4] = {11,12,13,14};      //  одномерных массивов

int x2[4] = {21,22,23,24};    

int *m[3] = {x0, x1, x2,};   // Создание массива указателей  

int i,j;

for (i=0; i<3; i++) {

  printf("\n Cтрока %d) ", i+1);

  for (j=0; j<4; j++)

    printf("%3d", m[ i ] [ j ]);

 }

}

Результаты работы программы:

Cтрока 1)  1   2   3   4

Cтрока 2) 11 12 13 14

Cтрока 3) 21 22 23 24

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

#include <stdio.h>

void main()

{

int i, j;

int m[3][4] = { { 1, 2, 3, 4}, {11,12,13,14}, {21,22,23,24} };

for (i=0; i<3; i++) {

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

  for (j=0; j<4; j++)

    printf(" %3d",m[ i ] [ j ]);

}

}

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

int m[3][4] = {1, 2, 3, 4, 11, 12, 13, 14, 21, 22, 23, 24};

Замена скобочного выражения  m[3][4] на m[12] здесь не допускается, так как массив указателей не будет создан.

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

Очевидна и схема размещения такого массива в памяти – последовательное (друг за другом) размещение «строк» – одномерных массивов со значениями (векторная организация памяти).

Обращению к элементам массива при помощи операции индексации m[i][j] соответствует эквивалентное выражение, использующее адресную арифметику –  *(*(m+i)+j).

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

10.7. Адресная функция

Векторная память поддерживается почти всеми языками высокого уровня и предназначена для хранения массивов различной размерности и различных размеров. Каждому массиву выделяется непрерывный участок памяти указанного размера. При этом элементы, например, двухмерного массива X размерностью n1n2 размещаются в ОП в следующей последовательности:

Х(0,0), Х(0,1), Х(0,2),... Х(0, n2–1), ..., Х(1,0), Х(1,1), Х(1,2),... Х(1, n2–1), ..., Х(n1–1,0), Х(n1–1,1), Х(n1–1,2), ..., Х(n1–1, n2–1).

Адресация элементов массива определяется некоторой адресной функцией, связывающей адрес и индексы элемента.

Пример адресной функции для массива Х:

 K(i, j) = n2*i + j;

где  i = 0,1,2,... ,(n1–1); j = 0,1,2,... ,(n2–1); j – изменяется в первую очередь.

Адресная функция двухмерного массива A(n,m) будет выглядеть так:

  N1 = K(i, j) = m*i + j,

i=0,1,..., n–1;  j=0,1,... , m–1 .

Тогда справедливо следующее:

  A(i, j)  B(K(i, j)) = B(N1),  

B – одномерный массив с размером N1 = n*m.

Например, для двухмерного массива A(2,3) имеем:

(0,0)

(0,1)

(0,2)

(1,0)

(1,1)

(1,2)

– индексы массива А;

0

1

2

3

4

5

– индексы массива В.

Проведем расчеты:

i = 0, j = 0 N1 = 3*0+0 = 0 B(0)

i = 0, j = 1 N1 = 3*0+1 = 1 B(1)

i = 0, j = 2 N1 = 3*0+2 = 2 B(2)

i = 1, j = 0 N1 = 3*1+0 = 3 B(3)

i = 1, j = 1 N1 = 3*1+1 = 4 B(4)

i = 1, j = 2 N1 = 3*1+2 = 5 B(5)

Аналогично получаем адресную функцию для трехмерного массива Х(n1, n2, n3):

 K(i, j, k) = n3*n2*i + n2*j + k ,

где  i = 0,1,2,... ,(n1–1); j = 0,1,2,... ,(n2–1); ); k = 0,1,2,... ,(n3–1); значение k – изменяется в первую очередь.

Для размещения такого массива потребуется участок ОП размером (n1*n2*n3)*sizeof(type). Рассматривая такую область как одномерный массив Y(0,1,..., n1*n2*n3), можно установить соответствие между элементом трехмерного массива X  и элементом одномерного массива Y:

  X(i, j, k)    Y(K(i, j, k)) .

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

10.8. Работа с динамической памятью 

Указатели чаще всего используют при работе с динамической памятью, которую иногда называют «куча» (перевод английского слова heap). Это свободная память, в которой можно во время выполнения программы выделять место в соответствии с потребностями. Доступ к выделенным участкам динамической памяти производится только через указатели. Время жизни динамических объектов – от точки создания до конца программы или до явного освобождения памяти.

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

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

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

10.9. Библиотечные функции

Функции для манипулирования динамической памятью в стандарте Си следующие:

void *calloc(unsigned n, unsigned size); – выделение памяти для размещения n объектов размером size байт и заполнение полученной области нулями; возвращает указатель на выделенную область памяти;

void  *malloc (unsigned  n) – выделение области памяти для размещения блока размером n байт; возвращает указатель на выделенную область памяти;

void *realloc (void *b, unsigned n) – изменение размера размещенного по адресу b блока на новое значение n и копирование (при необходимости) содержимого блока; возвращает указатель на перераспределенную область памяти; при возникновении ошибки, например, нехватке памяти, эти функции возвращают значение NULL, что означает отсутствие адреса (нулевой адрес);

coreleft (void) – получение размера свободной памяти в байтах только для MS DOS (используется в Borland C++), тип результата: unsigned – для моделей памяти tiny, small и medium; unsigned long – для моделей памяти compact, large и huge;

void   free (void   *b) – освобождение блока памяти, адресуемого указателем b.

Для использования этих функций требуется подключить к программе в зависимости от среды программирования заголовочный файл alloc.h или malloc.h.

__________________________________________________________________

В языке С++ введены операции захвата и освобождения памяти new и delete, рассматриваемые в разд. 16.4.

__________________________________________________________________

10.10. Пример создания одномерного динамического массива

В  языке Си размерность массива при объявлении должна задаваться константным выражением.

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

Память под такие массивы выделяется с помощью функций mallос и calloc (операцией new) во время выполнения программы. Адрес начала массива хранится в переменной-указателе. Например:

int n = 10;

double *b = (double *) malloc(n * sizeof (double));

В примере значение переменной n задано, но может быть получено и программным путем.

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

Обращение к элементу динамического массива осуществляется так же, как и к элементу обычного – например а[3]. Можно обратиться к элементу массива и через косвенную адресацию – *(а + 3). В любом случае происходят те же действия, которые выполняются при обращении к элементу массива, декларированного обычным образом.

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

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

Пример работы с динамическим массивом:

#include <alloc.h>

void main()

{

double *x;

int n;

printf("\nВведите размер массива – ");

scanf("%d", &n);

if ((x = (double*)calloc(n, sizeof(*x)))==NULL)  { // Захват памяти

puts("Ошибка ");

      return;

}

     ...

// Работа с элементами массива

     ...

free(x);       // Освобождение памяти

}

__________________________________________________________________

Примеры создания одномерного и двухмерного динамических массивов с использованием операций new и delete можно посмотреть в разд. 16.4.

__________________________________________________________________

10.11. Пример создания двухмерного динамического массива

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

. . .

int  **m, n1, n2, i, j;

puts(" Введите размеры массива (строк, столбцов): ");  

scanf(“%d%d”, &n1, &n2);

// Захват памяти для указателей – А (n1=3)

m = (int**)calloc(n1, sizeof(int*));   

for (i=0; i<n1; i++)  // Захват памяти для элементов – B (n2=4)

 *(m+i) = (int*)calloc(n2, sizeof(int));  

for ( i=0; i<n1; i++)   

for ( j=0; j<n2; j++)   

  m[i] [j] = i+j;  //  *(*(m+i)+j) = i+j;

. . .

for(i=0; i<n; i++) free(m[i]);   // Освобождение памяти

free(m);     

. . .

ГЛАВА 11. Функции пользователя

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

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

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

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

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

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

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

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

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

11.1. Декларация функции

Как и любой объект программы на языке Си, пользовательские функции необходимо декларировать. Объявление функции пользователя, т.е. ее декларация, выполняется в двух формах – в форме описания (объявления) и в форме определения, т.е. любая пользовательская функция должна быть объявлена и определена.

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

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

В стандарте языка используется следующий формат декларации (объявления) функций:

тип_результата   ID_функции (список);

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

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

Пример объявления функции fun, которая имеет три параметра типа int, один параметр типа double и возвращает результат типа double:

double fun(int, int, int, double);

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

Полное определение (реализация) функции имеет следующий вид:  

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

                     {

    код функции

   return  выражение;

                     }

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

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

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

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

Функция может не иметь параметров, но круглые скобки необходимы в любом случае. Если у функции отсутствует список параметров, то при декларации такой функции желательно в круглых скобках указать void. Например, void main(void){ ... }.

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

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

 int  min (int x, int y)

{

return (x<y) ? x : y;

}

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

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

11.2. Вызов функции

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

Простейший вызов функции имеет следующий формат:

ID_функции (список аргументов);

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

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

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

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

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

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

#include <stdio.h>

void f1(int);

void main(void)

{

f1(5);

}

void f1(int i)

{

int m=0;

puts(" n  m  p ");

while (i--) {

static int n = 0;

 int p = 0;

printf(" %d  %d  %d \n", n++ , m++ , p++);

}

}

Статическая переменная n будет создана в сегменте данных ОП и проинициализируется нулем только один раз при первом выполнении оператора, содержащего ее определение, т.е. при первом вызове функции f1. Автоматическая переменная m инициализируется при каждом входе в функцию. Автоматическая переменная р инициализируется при каждом входе в блок цикла.

В результате выполнения программы получим

n  m  p

0  0  0

1  1  0

2  2  0

3  3  0

4  4  0

11.3. Передача аргументов в функцию

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

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

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

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

Пример функции, в которой меняются местами значения x и y:

void  zam(int *x, int *y)

{

int  t = *x;  

*x = *y;

*y = t;

}

Участок программы с обращением к данной функции:

void zam (int*, int*);

void main (void)  

{

int a=2, b=3;

printf(" a = %d , b = %d\n", a, b);

zam (&a, &b);

printf(" a = %d , b = %d\n", a, b);

}

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

a = 2 , b=3

a = 3 , b=2

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

void f1(int, const double);

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

11.4. Операция typedef

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

typedef    тип   новое_имя ;

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

typedef unsigned int UINT; – здесь UINTновое имя;

typedef char M_s [101]; – здесь M_s – тип пользователя, определяющий строки, длиной не более 100 символов.

Декларации объектов введенных типов будут иметь вид

UINT   i, j;    две переменные типа unsigned int ;

M_s    str[10];  массив из 10 элементов, в каждом из которых можно хранить по 100 символов.

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

11.5. Указатели на функции

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

Рассмотрим методику работы с указателями на функции.

1. Как и любой объект языка Си, указатель на функции необходимо декларировать. Формат объявления указателя на функции следующий:

 тип (*переменная-указатель)(список параметров);

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

Например, объявление вида

double (*p_f )(char, double);  

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

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

 переменная-указатель = ID_функции;

Например, имеется функция с прототипом: double f1(char, double); тогда операция

p_f = f1;

установит указатель p_f на данную функцию.

3. Вызов функции после установки на нее указателя выглядит так:

 (*переменная-указатель)(список аргументов);

или

 переменная-указатель (список аргументов);

После таких действий кроме стандартного обращения к функции:

 ID_функции(список аргументов);

появляется еще два способа вызова функции:

 (*переменная-указатель)(список аргументов);

или

 переменная-указатель (список аргументов);

Последняя запись справедлива, так как p_f также является адресом начала функции в оперативной памяти.

Для нашего примера к функции f1 можно обратиться следующими способами:

f1(‘z’, 1.5);  – обращение к функции по имени (ID);

(* p_f)(‘z’, 1.5); – обращение к функции по указателю;

p_f(‘z’, 1.5); – обращение к функции по ID указателя.

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

Пример: написать функцию вычисления суммы sum, обозначив слагаемое формальной функцией fun(x). При вызове функции суммирования передавать через параметр реальное имя функции, в которой задан явный вид  слагаемого. Например, пусть требуется вычислить две суммы:

 и     .

Поместим слагаемые этих сумм в пользовательские функции f1 и f2 соответственно. При этом воспользуемся операцией typedef, введя пользовательский тип данных: указатель на функции p_f, который можно устанавливать на функции, возвращающие результат double и имеющие один параметр типа double.

Тогда в списке параметров функции суммирования достаточно будет указать фактические идентификаторы функций созданного типа p_f.

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

. . .

typedef double (*p_f)(double);

double sum(p_f, int, double);  // Декларации прототипов функций 

double f1(double);

double f2(double);
void main(void)

{

double x, s1, s2;

int n;

puts (" Введите кол-во слагаемых n и значение x: ");

scanf (" %d %lf ", &n, &x);

s1 = sum (f1, 2*n, x);

s2 = sum (f2, n, x);

printf("\n\t N = %d , X = %lf ", n, x);

printf("\n\t Сумма 1 = %lf\n\t Сумма 2 = %lf ", s1, s2);

}

/*  Первый параметр функции суммирования – формальное имя функции,  введенное с помощью typedef типа */

double sum(p_f  fun, int n, double x)  {

double s=0;

for(int i=1; i<=n; i++)  

s+=fun(x);

return s;

}
//––––––––––––––
Первое слагаемое –––––––––––––––––––

double f1(double r) {

return r/5.;

}

//–––––––––––––– Второе слагаемое ––––––––––––––––––––

double f2(double r) {

return r/2.;

}

В заключение рассмотрим оптимальную передачу в функции одномерных и двухмерных массивов.

Передача в функцию одномерного массива:

void main(void)  

{

int vect[20];

fun(vect);

}

void   fun( int v[ ]) {

 

}

При использовании в качестве параметра одномерного массива в функцию передается указатель на его первый элемент, т.е. массив всегда передается по адресу и параметр v[ ] преобразуется  в *v. Поэтому этой особенностью можно воспользоваться сразу:

void   fun( int *v) {

 …

}

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

 void   fun( int v[20]) {

. . .

}

В случае передачи массива символов, т.е. строки, ее фактическую длину можно определить по положению признака окончания строки (нуль-символа) через стандартную функцию strlen.

Передача в функцию двухмерного массива:

Если  размеры  известны  на этапе компиляции, то

void f1(int m[3][4])  {

int i, j;

for ( i = 0; i<3; i++)

          for ( j = 0; j<4; j++)

. . .     // Обработка массива

}

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

void main(void)

{

  int mas [3][3]={{1,2,3}, {4,5,6}};

    …

   fun (mas);

    …

}

void fun( int m[ ][3]) {

…      

}

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

   

void fun( int**, int, int);

void main()

{

int **mas, n, m;

 ...

   fun (mas, n, m);

    …

}

void fun( int **m, int n, int m)  {

. . .     // Обработка массива

}

11.6. Рекурсивные функции

Рекурсивной (самовызываемой или самовызывающей) называют функцию, которая прямо или косвенно вызывает сама себя.

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

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

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

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

1. При каждом вызове в функцию передавать модифицированные данные.

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

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

Пример 1. Заданы два числа a и b, большее из них разделить на меньшее, используя рекурсию.

Текст программы может быть следующим:

 . . .

double proc(double, double);

void main (void)

{

double a,b;

puts(“ Введи значения a, b : ”);

scanf(“%lf %lf”, &a, &b);

printf(“\n Результат деления : %lf”, proc(a,b));

}

//––––––––––––––––––– Функция –––––––––––––––––––––––

double proc( double a, double b)   {

if ( a< b ) return proc ( b, a );

 else  return a/b;

}

Если  a больше b, условие, поставленное в функции, не выполняется и функция proc возвращает нерекурсивный результат.

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

Пример 2. Функция для вычисления факториала неотрицательного значения k (для возможных отрицательных значений необходимо добавить дополнительные условия).

double fact (int k) {

if ( k < 1 ) return 1;

else

 return  k * fact ( k – 1);

}

Для нулевого значения параметра функция возвращает 1 (0! = 1), в противном случае вызывается та же функция с уменьшенным на 1 значением параметра и результат умножается на текущее значение параметра. Тем самым для значения параметра k организуется вычисление произведения

k   *   (k–1)   *   (k–2)   * ... *  3  *  2  *  1  * 1

Последнее значение «1» – результат выполнения условия k < 1 при k = 0, т.е. последовательность рекурсивных обращений к функции fact прекращается при вызове  fact(0). Именно этот вызов приводит к последнему значению «1» в произведении, так как последнее выражение, из которого вызывается функция, имеет вид:  1 * fact( 1 – 1).

Пример 3. Рассмотрим функцию определения корня уравнения f(x) = 0 на отрезке [а, b] с заданной точностью eps. Предположим, что исходные данные задаются без ошибок, т.е. eps > 0, f(a)*f(b) < 0, b > а, и вопрос о возможности существования нескольких корней на отрезке [а,b] нас не интересует. Не очень эффективная рекурсивная функция для решения поставленной задачи приведена в следующей программе:

. . .

int counter = 0;   // Счетчик обращений к тестовой функции

//–––––––– Нахождение корня методом деления отрезка пополам ––––––––––

double   Root(double f(double), double a, double b, double eps)  {

double  fa = f(a), fb = f(b), c, fc;

if ( fa * fb > 0) {

printf("\n На интервале a,b НЕТ корня!");

exit(1);

}

с = (а + b) / 2.0;

fc = f(c);

if (fc == 0.0 ||  fabs(b – a) < = eps) return c;

return (fa * fс < 0.0) ? Root(f, a, c, eps) : Root(f, c, b, eps);

}

//–––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––

void main()

{

double x, a=0.1, b=3.5, eps=5е–5;

double fun(double) ;    // Прототип тестовой функции  

x = Root (fun, a, b, eps) ;

printf ("\n Число обращений к функции = %d . ", counter);

printf ("\n Корень = %lf  . ", x);

}

//–––––––––––––– Определение тестовой функции  fun –––––––––––––––––

double fun (double x) {

counter++;   // Счетчик обращений – глобальная переменная

return (2.0/x * соs(х/2.0));

}

Значения a, b и eps заданы постоянными только для тестового анализа полученных результатов, хотя лучше данные вводить с клавиатуры.

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

Число  обращений  к функции =  54 .

Корень  =  3.141601.

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

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

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

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

11.7. Параметры командной строки функции main 

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

int main () {

 

return 0;

}

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

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

 int main (int argc, char *argv[])  ...

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

Функция main может иметь и третий параметр argp, который служит для передачи параметров операционной системы (ОС), в которой выполняется программа, в этом случае ее заголовок имеет вид

int main (int argc, char *argv[], char *argp[])

Операционная система поддерживает передачу значений для параметров argc, argv, argp, а пользователь отвечает за передачу и использование фактических аргументов функции main.

Приведем пример программы печати фактических аргументов, передаваемых из ОС в функцию main и параметров оперативной системы.

int main ( int argc, char *argv[], char *argp[])

{  

  int i;

  printf ("\n Program Name %s", argv[0]);

  for( i=1; i <=argc; i++)

   printf ("\n Argument %d  =  %s", i, argv[i]);

  printf ("\n OC parametrs: ");

  while  (*argp) {  

  printf ("\n %s", *argp);

      argp++;

  }

  return 0;

}

Очевидно, что оформленная таким образом функция main() может вызываться рекурсивно из любой функции.

ГЛАВА 12. Классы памяти и область действия объектов

12.1. Классы памяти объектов в языке Cи

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

Класс памяти программного объекта определяет время ее существования (время жизни) и область видимости (действия) и может принимать одно из значений: auto, extern, static и register.

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

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

Имеется три основных участка программы, где можно декларировать   переменные:

– внутри функции (блока);

– в заголовке функции при определении параметров;

– вне функции.

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

Область действия локальных данных – от точки декларации до конца функции (блока), в которой произведена их декларация, включая все вложенные блоки.

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

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

Время жизни может быть постоянным – в течение выполнения программы, и временным – в течение выполнения функции (блока) программы.

12.2. Автоматические переменные

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

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

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

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

Принадлежность к этому классу можно также подчеркнуть явно, например: