1. введение в программирование
1.1. Алгоритм, программа, формальные языки
Определение. Алгоритм (программа) – последовательность инструкций компьютеру, которые управляют его работой по обработке данных.
В этом определении подчеркнуты две особые черты алгоритма:
- Назначение алгоритма: обработка данных, какое-либо их изменение. Данные могут быть различных типов, например, текстовые, числовые, графические и прочие.
- Механизм алгоритма: обработка данных выполняется именно так, как управляет этим процессом программа, что и составляет главный принцип работы компьютера – принцип программного управления.
Укрупненная схема соотношения программы и обрабатываемых ею данных приведена на рис. 1.1.1.
Рис. 1.1.1. Укрупненная схема соотношения программы и данных.
В процессе выполнения сама программа (или ее часть), и данные (или их часть) находятся в оперативной памяти компьютера (ОЗУ).
Требования к программе (алгоритму):
- Детерминированность. Это точное, однозначное толкование любого шага алгоритма, отсутствие таких пунктов, которые могут быть поняты двояко.
- Результативность. Это свойство позволяет получить результат в любом случае, независимо от данных. Предполагает отсутствие тупиков: из любой нестандартной ситуации, возникшей при выполнении алгоритма, должен быть выход. Для данных, для которых нет решения, например, деление на 0, предусматривается особый ход алгоритма, пишутся обработчики ошибок, разрабатываются интерфейсные средства, которые позволят выбрать обходной маневр, изменить процесс, задать новые данные и прочее.
- Массовость. Это решение некоторого класса задач. Например, решение одного уравнения, это не алгоритмическая, а математическая задача, а решение в общем виде линейных или квадратичных уравнений, это алгоритмы.
Помимо этих требований, программа должна иметь удобный (дружественный) интерфейс, такое человеческое лицо. Требования хорошего интерфейса означают, что используются меню, мышь, окна диалога, справочная система, контекстные меню и т.д. Современные информационные технологии в полной мере обладают такими свойствами.
Большинство современных программ, это программы, управляемые событиями. К событиям относятся любое управляющее воздействие извне. Со стороны человека, сидящего за компьютером, происходит событие путем нажатия клавиш на клавиатуре или кнопок мыши, в ответ на это компьютер выполняет действие.
Определение. Программа, это всего лишь алгоритм, записанный на каком-либо алгоритмическом языке (формальном языке).
Можно сказать, что язык программирования, это инструмент для создания программ. Си++ – алгоритмический язык. Относится к категории формальных языков, которые разработаны специально для укрупненного описания алгоритмов или задач. Написание программ на машинно-ориентированном языке (например, ассемблере) процесс достаточно сложный. Он требует точности и занимает много времени. Использование языков программирования во много раз упрощает и ускоряет написание программ.
Сами языки входят в состав интегрированных сред разработчика (оболочек), которые позволяют программировать надежнее и быстрее. Например, интегрированная среда разработчика Turbo Pascal использует язык Pascal, среда Воrland C использует язык Си++, среды Quick Basic, Turbo Basic используют язык Basic.
В последнее время наибольшей популярностью пользуются среды визуальной разработки приложений, главной составляющей которых также являются языки программирования, например Delphi использует язык Pascal, C++ Вuilder и Visual C++ используют язык Си++, Visual Basic использует язык Basic.
В состав любой интегрированной среды разработчика входят, по меньшей мере, девять ниже перечисленных составляющих.
- Язык программирования. Это ядро интегрированной среды, все последующие являются его оболочкой.
- Редактор кода (Edit). Это обычный текстовый редактор, который имеет все средства редактирования текста, и используется для ввода и редактирования текстов программ.
- Компилятор (Compile). Выполняет перевод с формального языка в машинный язык.
- Компоновщик (Linker). Собирает программу пользователя из отдельных составляющих и библиотечных программ, строит выполнимый код.
- Отладчик кода (Debugger). Это средство работает на всех этапах выполнения программы. Позволяет обнаружить ошибки программиста.
- Справочная система (Help). Как правило, достаточно разветвлена, позволяет получить помощь и по средствам собственно языка программирования, и по работе в оболочке, и по ошибкам программиста.
- Менеджер файлов. Используется для выполнения обычных файловых операций над файлами текстов программ и файлами данных.
- Менеджер проектов. Используется для объединения многих файлов программы в единое целое (Project).
- Средства настройки среды и ее составляющих (Options).
Схема прохождения задачи в интегрированной среде разработки Си++ приведена на рисунке 1.1.2.
Рис. 1.1.2. Схема выполнения задачи в интегрированной среде разработки Си++
В двойной рамке показаны тексты, обрабатываемые в интегрированной среде, в одинарной рамке – модули их обработки, которые входят в состав интегрированной среды. Исходный текст программы на Си, это текстовый файл (один или несколько). Три обязательные этапа обработки текста программы, это препроцессорное преобразование, компиляция и компоновка. Задачей препроцессора является преобразование текста программы до ее компиляции. Препроцессор сканирует текст, находит в нем команды, называемые директивами препроцессора, и выполняет их. В результате происходят изменения в исходном тексте, вставки фрагментов текста и замена некоторых его фрагментов. Полученный текст называется полным текстом программы.
Далее этот текст проходит этап компиляции, на котором исходный код преобразуется во внутреннее машинное представление, некоторую последовательность команд, которая уже понятна компьютеру.
На этапе компоновки происходит редактирование связей и сборка исполнимого текста программы. Компоновщик обрабатывает все вызовы библиотечных функций и выполняет их подключение. Таким образом, к компилированному исходному коду добавляются необходимые функции стандартных библиотек. Готовый код является исполнимым и может быть выполнен компьютером при его запуске.
Языки программирования, и Си++ в их числе, относятся к формальным языкам, задачей которых является описание алгоритмов и функций, выполняемых компьютером. Проведем аналогию изобразительных средств естественных языков и формальных.
В естественном языке (русском, английском) схема построения его конструкций примерно такова, как на рис. 1.1.3.
алфавит
|
слова
|
предложения
|
тексты
|
произведения
|
Набор неделимых символов для построения конструкций языка.
|
Используются правила грамматики.
|
Добавлены знаки препинания.
Используются правила орфографии, пунктуации.
|
Добавлено деление на разделы (абзац, параграф).
Используются правила.
|
Добавлены составляющие структуры (глава, том).
|
Рис. 1.1.3. Попытка разложить на составляющие изобразительные средства естественного языка
Естественные языки отличает богатство правил и изобразительных средств, поэтому написание программ на естественном языке невозможно.
В формальном языке (языке программирования) схема построения его конструкций примерно такова, как на рис. 4.
алфавит
|
лексемы (выражения)
|
операторы (инструкции)
|
программы
(блоки, функции)
|
технологии (комплексы)
|
Входят все символы языка, в том числе, знаки.
|
Имеют смысл слов. Строятся по определенным правилам.
|
Имеют смысл предложений. Описывают один шаг алгоритма.
|
Имеют смысл текста. Делятся на структурные составляющие, описывающие самостоятельные алгоритмы.
|
|
Рис 1.1.4. Попытка разложить на составляющие изобразительные средства формального языка
Формальные языки отличает скудость синтаксических правил и небольшой набор изобразительных средств, соответственно целям этих языков. Система правил алгоритмического языка намного беднее систем правил естественных языков. В ее состав входят только две группы:
- синтаксис – формальный набор правил, определяющий способ построения любых конструкций языка.
- семантика – множество правил, определяющих смысл синтаксических конструкций.
1.2. Начальные сведения о языке модульного программирования Си++
К базовым понятиям языка Си++ относятся понятия об алфавите, правилах построения лексем и операторов языка, однако чтобы писать хорошие программы, нужно отчетливо представлять себе механизмы Си++, поэтому в изложении мы отойдем от предложенной схемы.
1.2.1. Алфавит языка Си++ и лексемы
Алфавит языка содержит четыре группы символов.
- Буквы. Разрешается использовать буквы латинского алфавита, прописные (A..Z) и строчные (a..z).
Русские буквы не входят в алфавит, но используются в комментариях и текстовых константах, то есть там, где они не влияют на смысл программы.
- Цифры. Используются арабские цифры 0, 1, .. 9.
- Специальные символы. Они могут быть разделены на подгруппы:
- знаки препинания (разделители): , ; . : ;
- знаки операций: + – * / % & | ? ! < = > ;
- парные скобки: [ ] { } ( ) " " ' '
- прочие символы: _ # ~ ^ .
- Невидимые символы. Они могут считаться разделителями, их особенность в том, что символы существуют (каждый имеет код), но в редакторе не видны. Это такие символы как пробел, табуляция, разделитель строк. Их общее название – обобщенные пробельные символы.
Лексема, это единица текста (конструкция, слово), воспринимаемая компилятором как единое неделимое целое. Можно выделить пять классов лексем.
- Имена (идентификаторы) для именования произвольных объектов программы, например, x1, Alpha, My_file.
- Служебные (ключевые) слова, обозначающие конструкции языка (имена операторов), например, for, while, do.
- Константы, например, 1, 12.5, "Василий".
- Операции (знаки операций), например, ++, >=, !=, >>.
- Разделители (знаки пунктуации), например, [ ], ( ), { }.
1.2.2. Основные понятия языка
Определение. Данные – все, что подлежит обработке с помощью программы.
Классификация данных может быть выполнена по нескольким категориям.
- По типу. Принадлежность каждого данного к какому-либо типу обязательна. На первом уровне типы данных можно разделить на базовые, то есть такие, правила организации которых предопределены реализацией языка, и конструируемые, то есть те, которые пользователь строит по определенным правилам для конкретной задачи.
- По способу организации. Для каждого из простых (базовых) типов каждое данное может быть неизменяемым или изменяемым. По способу организации данные делятся на два класса.
- Константа – данное, которое не меняет своего значения при выполнении программы и присутствует в тексте программы явным образом. Тип константы определен ее записью.
- Переменная – данное, которое изменяется при выполнении программы, и в тексте присутствует своим именем (идентификатор). Тип переменной величины должен быть объявлен в тексте программы.
1.2.3. Константы в языке Си++
Итак, константа представляет значение, которое не может быть изменено. Константы обладают типом, и тип определяется записью константы. Синтаксис языка выделяет пять типов констант: целые, действительные (вещественные) символьные, перечислимые, нулевой указатель.
1.2.3.1.Целые константы
Синтаксис языка позволяет использовать константы трех систем счисления: десятичные, восьмеричные, шестнадцатеричные. Основание определяется префиксом в записи константы. По умолчанию основание 10, префикс 0 предваряет восьмеричную константу, префикс 0х или 0Х предваряет шестнадцатеричную константу. В остальном, запись целых констант соответствует общепринятой, примеры записи целых констант приведены в таблице 1.2.3.1.1.
Десятичные
|
Восьмеричные
|
Шестнадцатеричные
|
1 127
0 -256
|
012
-014
|
0xa
-0x10
|
Таблица 1.2.3.1.1. Примеры записи целых констант
Для представления целых чисел используются константы, которые представлены в памяти компьютера в форме с фиксированной точкой. Способ хранения данных накладывает ограничения на размер хранимых констант.
1.2.3.2. Действительные константы
Для представления вещественных чисел используются константы, которые представлены в памяти компьютера в форме с плавающей точкой. Для каждой такой константы определены следующие составляющие:
, где
М – мантисса, вещественное число,
10 – основание системы счисления, в синтаксисе Си++ заменяется буквой е или Е,
р – показатель десятичной степени, целое число, возможно, со знаком.
Некоторые из составляющих могут быть опущены.
Примеры записи действительных констант.
44. 3.1415926 44.е0 .31459Е1 0.0
1.2.3.3. Символьные константы
Символьная константа, это лексема, которая содержит произвольный символ, заключенный в одинарные кавычки (апостроф). Хранится в одном байте памяти в виде целого числа, определяющего индивидуальный код символа.
Примеры символьных констант: '!' 'ф' '.' 's' 'А' 'а' '+' '2'.
Символьной константой может быть любой символ, изображаемый на экране в текстовом режиме, или не имеющий отображения (пробельный символ).
Существуют также управляющие символы (ESC -последовательности), которые используются в строковых константах и переменных. Они имеют признак '\' (backslash или слэш), который радикально изменяет смысл символа, заставляя его выполнять управляющее воздействие. Вот их неполный список:
'\0' нулевой символ;
'\n' перевод строки;
'\r' возврат каретки;
'\t' табуляция;
'\" апостроф;
'\\' обратный слэш;
'\ddd' восьмеричное представление символьной константы, где ddd – ее восьмеричный числовой код, например, '\017' или '\233'.
'\xhh' или '\Xhh' шестнадцатеричное представление символьной константы, где hh – ее числовой код, например, '\x0b' или '\x1F'.
Символьные константы имеют целый тип, и могут использоваться в выражениях.
1.2.3.4. Строковые константы
Формально строки не относятся к константам языка Си, а представляют отдельный тип его лексем. Строковая константа определяется как набор символов, заключенных в двойные кавычки " ".
Например,
#define STR "Программа"
В конце строковой константы автоматически добавляется нулевой символ '\0'. В тексте строки могут встретиться ESC – последовательности, которые будут выполнять управляющее воздействие.
Например,
"\nПри выводе \nтекст может быть \nразбит на строки.".
Здесь \n выполняет управляющее воздействие.
При хранении строки все символы, в том числе управляющие, хранятся последовательно, и каждый занимает ровно один байт. В конце строки добавлен нулевой символ.
1.2.3.5. Типы числовых констант и модификаторы типов.
Для модификации типа констант используются префиксы и суффиксы, добавляемые к значению константы. Суффикс l (L) от слова long увеличивает длину данного в два раза, суффикс u(U) от слова unsigned делает константу беззнаковой, то есть положительной, при этом увеличивается число разрядов, отводимых под запись числа, а значит, и его возможное значение. Например, длинные константы 12345l -54321L. Беззнаковые константы 123u, 123U. Шестнадцатеричная длинная константа 0xb8000000l. Восьмеричная длинная беззнаковая константа 012345LU.
1.2.4. Переменные в языке Си++
В языках программирования одним из основных понятий является объект. Определение. Объект, это некоторая сущность, обладающая определенным набором свойств, и способная хранить свое состояние.
Например, константы, это частный случай объекта. Переменная, это тоже объект, для использования которого необходимо определить три характеристики:
- Имя.
- Тип.
- Значение (не обязательно).
Имя переменной (идентификатор) обозначает в тексте программы величину, изменяющую свое значение. Для каждой переменной в памяти компьютера выделяется некоторая область памяти, способная хранить значение. Имя позволяет осуществить доступ к области памяти, хранящей значение переменной.
Тип переменной определяет размер выделяемой памяти и способ хранения значения.
Значение переменной не определено вначале, может изменяться при присваивании переменной значения.
Имя объекта (переменной) включает в себя латинские буквы, цифры, знак подчеркивания _. Первым символом должна быть буква. Примеры идентификаторов: x1, price, My_file1, alpha, PI. В качестве имен рабочих переменных используются простые имена i, j, k1.
Ограничения.
- Длина имени ограничена, и содержит не более 32-х символов.
- В качестве имен нельзя использовать ключевые слова Си++, например, названия операторов for, if, do.
- В качестве имен не рекомендуется использовать имена объектов стандартных библиотек, например, имена функций sin, sqrt, pow.
- Имена, которые начинаются со знака подчеркивания, зарезервированы для использования в библиотеках и компиляторах, поэтому их не следует выбирать в качестве прикладных имен, например, _acm, _AX, _BX.
Замечания.
- Рекомендуется использовать имена переменных, отражающие их первоначальный смысл, например, Count (количество), X_coord (координата по х), Old_value_of_long (предыдущее значение длины).
- Большие и маленькие буквы считаются различными. Так, разными объектами будут переменные, имеющие имена Alpha и alpha.
Идентификаторы, зарезервированные в языке, являются служебными (ключевыми) словами. Следовательно, их нельзя использовать как имена свободных объектов программы. Это типы данных, квалификаторы типа, классы памяти, названия операторов, модификаторы, псевдопеременные.
Перечень служебных слов приведен в таблице 1.2.4.1.
Типы данных
|
char double enum float int long short struct signed union unsigned void typedef
|
Квалификаторы типа
|
const volatile
|
Классы памяти
|
auto extern register static
|
Названия операторов
|
break continue do for goto if…else return switch while case default sizeof
|
Модификаторы
|
asm far huge near interrupt pascal и другие
|
Псевдопеременные
|
Это названия регистров _AH _DS _DX и другие
|
Таблица 1.2.4.1. Служебные слова в Си++
1.2.5. Типы данных в Си
Каждое данное, независимо от способа организации, обладает типом. Тип данного очень важен, так как он определяет:
- Механизм выделения памяти для записи значений переменной.
- Диапазон значений, которые может принять переменная.
- Список операций, которые разрешены над данной переменной.
Тип любого данного программы должен быть объявлен обязательно. Для констант тип определен явно записью константы в тексте программы. Для переменных тип должен быть задан в объявлении объекта (переменной). При объявлении переменной происходит выделение области памяти, размер которой определен типом переменной, и в которой хранится значение переменной.
Классификация типов данных:
- Простые типы (базовые, скалярные, внутренние, предопределенные).
- Конструируемые типы (массивы, строки, структуры, функции и пр.).
Основные типы данных определены ключевыми словами, список которых приведен в таблице 1.2.5.1.
Имя типа
|
Значение типа
|
Размер
|
Диапазон значений
|
char
|
Символьный (целое)
|
1 байт
|
-128 – 127
|
int
|
Целый
|
2 байта
|
-32768 – 32767
|
float
|
С плавающей точкой (действительный)
|
4 байта
|
3.4e-38 – 3.4e38
|
double
|
Двойной точности (действительный)
|
8 байт
|
1.7e-308 – 1.7e308
|
Таблица 1.2.5.1. Ключевые слова, определяющие основные типы данных
Замечание: в Borland С++ для определения размера памяти есть операция sizeof(), которая поможет узнать размер памяти, занимаемый объектом, например, sizeof(int), sizeof(double), sizeof(my_obj). В качества аргумента операции можно указать имя типа или имя объекта.
Существует специальный тип данных, который должен быть отнесен к базовым, но имеет существенные особенности. Это тип void. По данное такого типа говорят, что оно «не имеет никакого типа». На самом деле тип void используется при работе с динамическими данными, и может адресовать пространство произвольного размера, где может быть размещено данное любого типа. Значит, можно также считать, что данное типа void может быть представлено как «данное любого типа».
Основные типы данных могут быть изменены (модифицированы) с использованием вспомогательных ключевых слов:
long (длинный) увеличивает объем выделяемой памяти в два раза.
unsigned (без знака) не использует знаковый байт, за счет чего хранимое значение может быть увеличено.
Так, если тип данного int, то диапазон его значений [-32768 – 32767], а если unsigned или unsigned int, то[0 – 65535].
Если тип char, то диапазон его значений [-128 – 127], а если unsigned char, то [0 – 255].
1.2.5.1. Объявление переменных
Каждая переменная должна быть объявлена в тексте программы перед первым ее использованием. Начинающим рекомендуется объявлять переменные в начале тела программы перед первым исполнимым оператором. При объявлении переменной указывается ее тип и имя:
имя_типа имя_переменной;
Можно одновременно объявить несколько переменных, тогда в списке имен они отделяются друг от друга запятой, например:
int a, b, c; // Целые со знаком.
char ch, sh; // Однобайтовые символьные.
long l, m, k; // Длинные целые [-2147483648 – 2147483647]
float x, y, z; // Вещественные.
long double u,v,w; // Длинные двойные.
Пусть в программе объявлены переменные:
int a;
float b;
char c;
На этапе компиляции для этих переменных в ОЗУ будет выделена память для записи значений переменных, как показано на рис. 1.2.5.1.1. Имя переменной определяет адрес, выделенной для нее памяти Тип переменной определяет способ хранения переменной в памяти.
Рис. 1.2.5.1.1. Схема выделения памяти для записи значений переменных
Тип переменной определяет не только диапазон ее возможных значений, но и операции, разрешенные над этой величиной.
1.2.5.2. Инициализация переменных
Переменные, объявленные в программе, не имеют значения, точнее, имеют неопределенные значения. Так как память, выделенная под запись переменных, не очищается, то значением переменной будет «мусор» из того, что находилось в этой памяти ранее. Инициализация, это прием, применяя который можно присвоить значения переменным при их объявлении. Синтаксически инициализация выглядит так:
имя_типа имя_переменной = начальное значение;
Например,
float pi = 3.1415;
unsigned Year = 2004;
1.2.5.3. Именованные константы
В Си++, кроме переменных, можно использовать именованные константы, то есть константы, имеющие фиксированные названия (имена). Имена могут быть произвольными идентификаторами, не совпадающими с другими именами объектов. Принято использовать имена из больших букв и знаков подчеркивания, что визуально отличает имена переменных от имен констант. Способов определения именованных констант три.
Первый способ, это константы перечисляемого типа, имеющие синтаксис:
enum тип_перечисления {список_именованных_констант};
Здесь тип_перечисления, это его название, необязательный произвольный идентификатор, список_именованных_констант, это разделенная запятыми последовательность вида: имя_константы = значение_константы или просто значение константы.
Примеры:
enum BOOL {FALSE, TRUE };
enum DAY {SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY};
enum {ONE = 1, TWO, THREE, FOUR, FIVE};
Если в списке нет элементов со знаком «=», то значения констант начинаются с 0 и увеличиваются на 1 слева направо, так, FALSE равно 0, TRUE равно 1. SUNDAY равно 0, и так далее, SATURDAY равно 6. Если в списке есть элементы со знаком «=», то каждый такой элемент получает соответствующее значение, а следующие за ним увеличиваются на 1 слева направо, так значение ONE = 1, TWO = 2, THREE = 3, FOUR = 4, FIVE = 5.
Второй способ, это именованные константы, заданные с ключевым словом const в объявлении вида:
const имя_типа имя_константы = значение_константы;
Ключевое слово const имеет более широкий смысл, и используется также в других целях, но здесь его суть в том, что значение константы не может быть изменено.
Примеры:
const float Pi = 3.1415926;
const long M = 999999999;
const float Eps = 0.1e–5;
Попытка присвоить значение любой из констант Pi, M, Eps будет неудачной. Обратите внимание, что здесь визуализация имен констант использованием больших букв не необходима.
Третий способ ввести именованную константу дает препроцессорная директива define, подробный синтаксис и механизм выполнения которой будет рассмотрен далее.
1.2.6. Операции и выражения в Си++
Определение. Операция – это символ или лексема вычисления значения.
Операции используются для формирования и вычисления значений выражений, и для изменения значений данных.
1.2.6.1. Классификация операций
Операции Си++ можно разделить на группы. В первую группу выделим основные операции с классификацией по типу значения, возвращаемого операцией. Их список приведен в таблице 1.2.6.1.1.
Арифметические операции
|
Логические операции
|
Поразрядные операции
|
+ сложение
|
> больше
|
<< сдвиг влево
|
– вычитание
|
< меньше
|
>> сдвиг вправо
|
* умножение
|
= = равно
|
|
/ деление
(для целых операндов– целая часть от деления)
|
!= не равно
|
|
% остаток от деления (только для целых )
|
>= больше или равно
|
|
|
<= меньше или равно
|
|
|
&& логическое «И»
|
& поразрядное «И»
|
|
|| логическое «ИЛИ»
|
| поразрядное «ИЛИ»
|
|
! логическое «НЕ»
|
~ поразрядная инверсия
|
Таблица 1.2.6.1.1. Классификация операций по типу возвращаемого значения.
Арифметические операции имеют обычный математический смысл. Для целого типа данных существуют две операции целочисленного деления: обычное деление / и остаток от деления %. Каждая из этих операций, примененная к данным целого типа, возвращает целочисленное значение. При использовании для отрицательных значений сохраняется знак числителя.
Примеры:
int x = 5, y = 3;
x / y // Результат = 1
x % y // Результат = 2
y / x // Результат = 0
y % x // Результат = 3
x = – 5 // Знак сохраняется
x /y // Результат = – 1
x % y // Результат = – 2
y / x // Результат = 0
y % x // Результат = 3
Логические операции предназначены для проверки условий и для вычисления логических значений. Заметим, что логического типа данных в Си++ нет, его имитирует тип int (условно принято, что 0 = Ложь, 1 = Истина). Логическими являются операции, возвращающие логическое значение. Их можно, в свою очередь, разбить на две группы:
1. Операции отношения. Связывают данные числовых или символьных типов, и возвращают логическое значение, например:
- >1 // Истина = 1
'a' > 'b' // Ложь = 0
x >= 0 // Зависит от х
y != x // Зависит от х и от у
Значение логической операции может быть присвоено целой переменной, например:
int a = 5, b = 2;
int c;
c = a>b; // c = 1
c = a<b; // c = 0
c = a= =b; // c = 0
c = a != b; // c = 1
Логические операции могут участвовать в записи выражения, наряду с арифметическими операциями, тогда при вычислении значения выражения важен порядок вычисления, например:
c = 10*(a > b); // a > b = 1, 10*1 = 10, значит, с = 10
c = 10*(10*a<b); // 10*a = 50, 50 < b = 0, 10 * 0 = 0, значит, с =0
c = 1 < a && a < 10; // 1 < a = 1, а < 10 = 1, значит, с = 1
2. Логические операции. Связывают данные логического типа (целые), и возвращают логическое значение. Это чистые логические операции конъюнкция &&, дизъюнкция || и отрицание ! , которые применяются к значениям операндов, имеющих логическое значение. Если при этом операндом является отношение, то его значение предварительно вычисляется, например:
// 1. Оба операнда (x > 0 и y > 0) одновременно истинны:
x > 0 && y > 0.
// 2. Хотя бы один операнд (x%2 = = 0 или // y%2 = = 0) истинен:
x%2 = = 0 || y%2 = = 0.
// 3. Вычисляется значение, обратное значению операнда (x*x+y*y <= r*r):
! (x*x+y*y <= r*r).
На обычном языке эти условия имеют простой естественный смысл, который можно выразить следующими фразами:
1. Значения x и y одновременно положительны.
2. Хотя бы одно из x и y четно (остаток от деления на 2 равен нулю).
3. Точка с координатами (x, y) находится вне круга радиуса r.
Поразрядные операции. Применяются к любому значению, хотя имеет смысл их применение для целочисленных операндов. Операции сдвига выполняют сдвиг побитно содержимого первого операнда на значение, указанное в качестве второго операнда. Обычная запись такой операции имеет вид X >> 1 или x << 2 .
Примеры.
int c;
int a = 4; // Побитно а = 100
c = a << 1; // а сдвигается влево на 1 байт = 1000, значит, с = 8
c = a >> 2; // а сдвигается вправо на 2 байта = 10, значит, c = 2
Поразрядные логические операции выполняются побитно над значениями бит операндов, например:
int b = 3;
c = a & b; // Побитно 100 & 011 = 000, значит, с=0
c = a | b; // Побитно 100 | 011 = 111, значит, с=7
c = ~a; // Побитно ~100 = 011, значит, с=3
Можно ввести классификацию операций СИ++ по числу операндов.
- Унарные операции имеют только один операнд, например,
+10, – а, ! (x>0), ~a.
- Бинарные операции имеют два операнда, например,
x + 5, x – y, b && c?, a >> 2.
- Тернарная операция имеет три операнда. Это операция условия, приведем ее синтаксис здесь.
логическое_выражение ? выражение1 : выражение2
Операция условия выполняется в два этапа. Сначала вычисляется значение первого операнда, то есть логического выражения. Затем, если оно истинно, вычисляется значение второго операнда, который назван «выражение1», оно и будет результатом, если же первый операнд ложен, вычисляется значение второго операнда, который назван «выражение2», и результатом будет его значение.
Например, для вычисления абсолютного значения некоторого i, можно использовать текст:
int abs;
abs = (i <= 0) ? – i : i;
Вторая группа операций, это операции изменения данных, их список приведен в таблице 1.2.6.1.2. Основной операцией в этой группе является операция присваивания =.
Основные операции
|
Арифметические операции с присваиванием
|
Операции сдвига с присваиванием
|
= операция присваивания
|
+= сложение с присваиванием
|
<<= сдвиг влево с присваиванием
|
++ увеличение на единицу
|
–= вычитание с присваиванием
|
>>= сдвиг вправо с присваиванием
|
– – уменьшение на единицу
|
*= умножение с присваиванием
|
|
|
/= деление с присваиванием
|
|
|
%= остаток от деления с присваиванием
|
|
Таблица 1.2.6.1.2. Операции изменения данных
Операция присваивания в Си++, это обычная операция, которая является частью выражения, но имеет очень низкий приоритет, и поэтому выполняется в последнюю очередь. Семантика этой операции заключается в том, что вычисляется выражение правой части и присваивается левому операнду. Значит, слева может быть только переменная величина, которая способна хранить и изменять свое значение (левостороннее выражение). В правой части, как правило, записывается выражение.
Примеры:
x =2; // х принимает значение константы
cond = x<=2; // cond принимает логическое значение
3 = 5; // Ошибка, слева константа.
x = x+1; // x принимает значение, увеличенное на 1
Расширением операции присваивания являются операции вычисления с присваиванием, которые еще называются «присваивание после», и позволяют сократить запись выражений, например:
x += 5; // Аналог записи x = x+5;
y *= 3 // Аналог записи y = y*3;
Пусть c = 5, a = 3;
c += a; // c = 8
c –= a; // c = 2
c *= 2; // c = 10
c /= 4; // c = 1
Необычными операциями являются операции увеличения и уменьшения на единицу, имеющие самостоятельные названия инкремент и декремент, например:
x++; // Аналог записи x = x + 1; или х += 1;
– –x; // Аналог записи x = x – 1; или х –= 1;
Как видно в примере, каждая из этих операций имеет 2 формы:
- префиксная: ++ x или – – х. При ее выполнении x изменяется прежде, чем выполняются другие операции, входящие в выражение.
- постфиксная: x ++ или х – –. При ее выполнении сначала выполняются все операции выражения, и только потом изменяется х.
1.2.6.2. Выражения в Си++
Определение. Выражение, это правило вычисления одного значения.
Формально выражение, это несколько операндов, объединенных знаками операций. В качестве операндов выступают константы, имена переменных, вызовы операций-функций. Кроме того, в запись выражения могут входит технические символы, такие как скобки ( ). Как правило, смыслом выражения является не только вычисление значения, но и присваивание его переменной, поэтому в записи выражения операция = , как правило, присутствует.
Поскольку все данные в Си++ (переменные, константы и другие объекты) имеют тип, то значение, вычисленное выражением, также будет иметь тип, который определяется типом операндов, например, как в таблице 1.2.3.2.1.
Операнды
|
Операции
|
Результат
|
Целые
|
+ – * / %
|
целый
|
Вещественные
|
+ – * / -
|
вещественный
|
Числовые или символьные
|
> < != == >= <=
|
логический (int)
|
Логические
|
&& ! ||
|
логический
|
Таблица 1.2.3.2.1. Примеры определения типа выражения.
Если тип вычисленного выражением значения не совпадает с типом переменной левой части, то происходит преобразование (приведение) типов, о котором далее в отдельном разделе.
1.2.6.3. Порядок вычисления выражений
Порядок вычисления выражений определяется рангом (приоритетом) входящих в него операции и правилами ассоциативности. Правило ассоциативности применяется при вычислении значений выражений, в которых несколько операций одного приоритета стоят подряд. Большинство операций левоассоциативны (→), то есть вычисляются слева направо естественным образом. Однако есть правоассоциативные операции (←), которые вычисляются справа налево, и в этом есть глубокий смысл. Так, раскрываются справа налево унарные операции. Так, раскрываются справа налево операции присваивания, что позволяет выполнить цепочку присваиваний, например:
х = y = z = 10;
Здесь цепочка раскрывается по порядку z = 10, затем y = z, затем x = y.
В таблице 1.2.6.3.1 приведена таблица приоритетов операций Си++.
Ранг (приоритет)
|
Операции
|
Ассоциативность
|
|
() [] –> . (операции разыменования)
|
→
|
|
! ~ + – ++ – – & * (тип) sizeof
|
←
|
|
* / % (бинарные)
|
→
|
|
+ – (бинарные)
|
→
|
|
<< >> (сдвиг)
|
→
|
|
< <= > >= (отношения)
|
→
|
|
== != (отношения)
|
→
|
|
&
|
→
|
|
^ (поразрядное исключающее или)
|
→
|
|
|
|
→
|
|
&&
|
→
|
|
||
|
→
|
|
? : (условная операция)
|
←
|
|
= *= /= %= += –= &= ^= |= <<= >>=
|
←
|
|
, (операция запятая)
|
→
|
Таблица 1.2.6.3.1. Полная таблица приоритетов операций Си++.
Из таблицы видно, что принятый ранг операций наиболее близок к математическому, также как и принятый порядок их вычисления. Так, умножение и деление (мультипликативные операции) старше сложения и вычитания (аддитивные операции). Унарные операции + и – старше бинарных, стало быть, знак операнда вычмсляется в первую очередь. Операция присваивания и ее клоны младше прочих, что позволяет выполнить присваивание только после того, как значение выражения вычислено полностью. Операции отношения младше арифметических операций, что позволяет использовать естественную запись логических выражений, например, x>0 && y>0. Здесь в первую очередь вычисляются значения отношений, которые затем являются операндами конъюнкции.
Операции ++ и – – приведены в таблице с рангом 2, но на самом деле эти операции имеют две формы:
- Префиксная: ++x, – – x. При этом операция инкремента (декремента) старше всех прочих, входящих в запись выражения, и выполняется в первую очередь.
- Постфиксная: x ++, x – –. При этом операция инкремента (декремента) младше всех прочих, входящих в запись выражения, и выполняется в последнюю очередь.
Пример.
int c, a = 5 b = 3;
c = a++; // c = 6, а = 5.
c = a++*3; // c = a*3 = 15, после этого а = 6.
с = – – а / 2; // – – a, a = 5, потом c = а / 2 = 2.
c = a+++b; // сначала с = a + b = 8, потом а++, а = 6.
c = a+++++b; // ++b, потом +, потом а++.
1.2.6.4. Приведение типов и преобразование типов в выражениях
Под приведением типов понимаются действия, которые компилятор выполняет в случае, когда в выражении смешаны операнды различных типов, чего, строго говоря, не следует допускать. Механизмы приведения типов в Си++ заключаются в том, что при вычислении значения выражения компилятор, не изменяя внутреннего представления данных, преобразует данное «меньшего» типа к «большему», где «величину» типа определяет размер выделенной для него памяти. Такое преобразование выполняется без ведома программиста, и может привести к потере данных.
Известно, что целое значение представлено в памяти точно, а вещественное приближенно.
int a = 5, b = 2;
float c;
c = a * 1.5; // В выражении операнды разных типов, автоматически
// включаются механизмы приведения типов, с = 7.5.
Для того, чтобы избежать потерь информации, в выражениях следует применять операцию явного преобразования типа, синтаксис которой:
(имя_типа) имя_переменной
Эта запись включается в те выражения, в которых необходимо выполнить преобразование перед вычислением. В этом случае программист явно указывает компилятору, что тот должен сделать.
int a = 5, b = 2;
float c;
с = (float) a / 2.0; // c = 5.0 / 2.0 = 2.5
b = (int) c / b; // b = 2 / 2 = 1
Под преобразованием типов в выражениях понимаются действия, выполняемые при присваивании. Пусть в этом же примере
int a = 5, b = 2;
float c;
с = a / b;
Хочется думать, что значение с будет равно 2.5, ведь оно вещественное, но порядок операций таков, что деление старше присваивания, и оно выполняется с операндами целого типа, и его результат = 2, то есть приведение типа будет выполнено только при присваивании.
Для явного преобразования типов используется известный прием:
c = (float) a / (float) b; // c = 2.5
При выполнении присваивания, если типы левой и правой части не совпадают, также происходит неявное преобразование. Си++ всегда пытается это сделать, и упрощенно можно считать, что преобразование происходит без потери данных от меньшего типа к большему, например от int к float или от char к int, и с потерей данных из большего типа к меньшему, например, от float к int. Это легко понять, если вспомнить, что тип данного, это объем занятой им памяти.
Явное преобразование выполняется при присваивании вида
имя = (тип) выражение;
Рекомендуется строго относиться к типам данных, не смешивать типы в выражениях, следить, чтобы тип левого операнда присваивания соответствовал типу выражения правой части.
1.2.7. Структура и компоненты простой программы на языке С++. Функция main
Программа на языке С++ состоит из одного или нескольких текстовых файлов с расширением «.с» или «.сpp». Если файлов несколько, они объединяются в файл проекта с расширением «.prj». Простейшие программы содержатся в одном текстовом файле.
Программа может состоять из одной или нескольких функций. Одна из функций должна иметь имя «main». С этой функции всегда начинается выполнение программы. Любая функция, кроме «main», вызывается из другой функции. При вызове функции могут быть переданы параметры (данные). По окончании выполнения функции в вызывающую функцию может быть возвращено значение (результат), а может не быть возвращено ничего. Примером функций являются библиотечные функции, например, sin (x), fabs (a).
Функция обладает типом, соответствующим возвращаемому значению. Если функция ничего не возвращает, ее тип void. Если функция не имеет аргументов, вместо них в списке параметров записывается слово void.
Пример.
void main (void) //Не имеет типа и не имеет параметров.
main () // тип функции по умолчанию будет int.
// аргументов нет, значит, их может быть
// произвольное число.
Пример функций, не возвращающих значения, это printf (), scanf ().
1.2.7.1. Комментарии
Комментарии предназначены для записи пояснений и примечаний к тексту программы. Не влияют на выполнение программы. Записываются на родном языке программиста.
В Си++ существуют два вида комментариев:
1. Многострочный комментарий записывается в любом месте текста программы в скобках вида /* */.
Переводит в разряд примечаний весь текст, заключенный между ними. Удобен при отладке программы, чтобы вырезать из текста отдельные фрагменты, не удаляя их физически.
2. Однострочный комментарий записывается в любой строке программы после сочетания символов //.
Комментирует весь текст до окончания строки. Используется для пояснений к строкам.
1.2.7.2.Структура файла программы из одной функции. Блок операторов
Текст программы, которая состоит из одной функции, содержит следующие составляющие, почти все они могут отсутствовать.
#Директивы препроцессора //Начинаются с # и записываются в одну строку.
Тип_функции main (параметры) // Заголовок функции.
{ // Блок тела функции.
определения объектов;
исполняемые операторы;
return выражение; // Если функция возвращает значение.
}
В Си++ объявление переменной (объекта) возможно не только в начале программы, но и в любом месте текста до первого обращения к ней. Областью действия такого объекта является только непосредственно охватывающий его блок. Как правило, так объявляют рабочие переменные.
Определение. Блок (операторов), это произвольная последовательность определений и операторов, заключенная в фигурные скобки:
{
// Блок;
}
Блок используется для укрупнения структуры программы. Точка с запятой в конце блока не ставится.
Текст программы на Си++ обладает структурой. Существует система правил корректной записи текста программ.
- Каждый оператор заканчивается знаком «;». Обычная ошибка начинающего – это знак «;», завершающий заголовки функций или операторов цикла. В первом случае синтаксическая ошибка распознается как отсутствие тела функции, во втором случае телом цикла является пустой оператор, что синтаксической ошибкой не является, и программа выполняется.
- Каждый оператор записывается в одну строку. Это не необходимо, но очень рекомендуется, так как позволяет структурировать текст программы, наглядно видеть ее алгоритм, и облегчает отладку при пошаговом исполнении.
- Блок, то есть произвольный фрагмент текста, заключенный в фигурные скобки { } размещается в любом месте программы. Использование блоков позволяет укрупнить структуру алгоритма.
- Структура программы подчеркивается отступами (опция редактора Indent). Этот простой способ позволяет визуально показать блоки, составляющие структуру алгоритма, всего лишь выделив их в тексте отступами в три позиции для каждой внутренней структуры. Так, отступами принято выделять тело блока, содержимое условного оператора, тело цикла, и любые внутренние вложенные структуры.
- Комментарии в тексте необходимы.
- Имена объектов программы выбираются осмысленно. Каждое имя подчеркивает назначение и логику объекта, например, имена библиотечных функций sin, abs, printf и прочих говорят сами за себя. Имена объектов, введенные программистом, подчеркивают их абстрактный смысл, например, Count, Square, Point.x, Point.y и так далее.
- Пробелы в тексте являются значащими только в составе текстовых констант. В тексте программы пробелы обязаны отделять друг от друга объекты. В остальных случаях их использование произвольно, например, лишние пробелы улучшают читабельность программы.
1.2.8. Директивы препроцессорной обработки
Начинаются со знака # и записываются в одной строке. Являются командами (директивами), выполняемыми препроцессором на стадии предварительной обработки текста программы, то есть до ее компиляции. Директив препроцессора достаточно много, на начальном этапе достаточно ознакомиться с двумя из них.
1.2.8.1.Директива #define
Используется для задания именованных констант и для задания строк подстановки.
Синтаксис:
#define имя выражение
Механизм действия директивы, – макроподстановки, то есть препроцессор сканирует весь текст программы и выполняет замены в тексте программы, везде вместо «имени» подставляя «выражение».
Замечание: имена define - определенных констант записываются большими буквами, чтобы визуально отличить их от имен переменных и других объектов.
Пример.
#define N 10 // По всему тексту вместо N число10.
#define PI 3.1416926 // Вместо PI его числовое значение.
#define STR "Строковая константа, подставляется в текст\n"
Директива define может определить не только именованную константу, но и выполнить макроподстановки, например:
Hапример:
#define N1 N+1 // Вместо N1 текст 10+1.
#define int long // В тексте все описания int заменятся long
Удобно использовать эту директиву, чтобы, например, ввести в употребление логические константы, которых нет в синтаксисе Си++:
#define TRUE 1
#define FALSE 0
Кроме того, если в замещаемом имени есть скобки, следующие за именем без пробела, то это макроопределение с параметрами, синтаксис которого:
#define имя (список_параметров) выражение
Например,
#define Cube(x) x*x*x
Имя здесь играет роль имени макроопределения, а параметров может быть несколько, тогда они отделяются запятыми. Между именем и списком параметров не должно быть пробела. Такое макроопределение может использоваться как функция, хотя макроподстановки не заменяют функции, и иногда могут привести к ошибкам.
Пример.
#define Cube(x) x*x*x // Макроопределение возведения в степень.
#include <stdio.h>
void main(void)
{
int а = 2;
printf("%d %d ", a, Cube(a));
}
1.2.8.2 Директива #include
Используется для замены в тексте путем добавления текста из других файлов в точку нахождения #include.
Синтаксис:
#include "filename"
#include <filename>
Механизм действия, это включение текста указанного файла в текущее место в программе. Включаемые файлы называются заголовочными, и содержат информацию, которая для программы глобальна.
#include "filename" осуществляет поиск файла сначала в текущем каталоге, а затем в системных каталогах. Так подключаются личные файлы программиста, содержащие произвольные тексты, например, константы, объявления или описания функций.
#include <filename> осуществляет поиск файла только в системных каталогах. Так подключаются стандартные заголовочные файлы, поставляемые в комплекте со стандартными библиотеками функций.
Каждая библиотечная функция имеет свое описание (прототип). Кроме того, в заголовочных файлах описаны многие константы, определения типов и макроподстановки. Имена стандартных заголовочных файлов, содержащих описания библиотечных функций, например, <stdio.h>, для того, чтобы использовать функции ввода и вывода, <math.h>, для того, чтобы использовать описания математических функций, и другие. Их описание можно найти в справочной системе. Следует понимать, что использование include не подключает к программе соответствующую библиотеку, а только включает в текст программы на глобальном уровне все нужные описания и объявления. Сами библиотечные функции подключаются к программе в виде объектного кода на этапе компоновки, когда компоновщик обрабатывает все вызовы функций, определяет, какие потребуются программе, и собирает исполнимый файл, включая в него объектные (компилированные) коды только тех функций, к которым выполняется обращение.
1.2.9. Ввод и вывод данных в СИ++. Начальные сведения
Определение. Ввести данное – означает присвоить произвольное значение переменной во время выполнения программы.
Определение. Вывести данное – означает напечатать на экране значение переменной при выполнении программы.
Простейший из способов ввода и вывода (обмена) данных – это форматированный, с определением правил размещения данных во входном – выходном потоке. Для реализации такого обмена необходима библиотека stdio.h (standart input output library), которая подключается к программе директивой #include <stdio.h>
Для ввода значения данного с клавиатуры (с эхо повтором на экране) используется функция scanf, синтаксис которой:
scanf ("форматная строка", список_ввода);
Здесь «список ввода» – имена переменных, значения которых будут введены с клавиатуры при выполнении функции scanf. Имена переменных предваряются символом &, который является признаком адресной операции, и означает, что введенное значение пересылается по адресу, определенному именем переменной. При вводе данные отделяются пробелами, или Enter’ом.
Для вывода значения данного на экран используется функция printf, синтаксис которой:
printf ("форматная строка", список_вывода);
Здесь «список вывода» – список имен переменных и выражений (в том числе констант), значения которых появятся на экране при выполнении функции printf.
Форматная (управляющая) строка, – это строка символов внутри двойных кавычек, содержащая управляющие символы и текст. При вводе данных функция scanf читает посимвольно текст из входного потока, распознает лексемы и преобразует их в машинное представление в соответствии с признаком формата, сопоставленного переменной, ожидающей данное. При выводе функция printf берет машинное представление значения переменной, и соответственно признаку формата, преобразует в текстовое представление, и выводит на экран.
Число управляющих символов равно числу объектов в списке ввода-вывода. Управляющий символ имеет признак %, и одно из следующих значений:
%d – ввод - вывод целого десятичного числа (int);
%u – ввод - вывод целого без знака (unsigned);
%f – ввод - вывод числа с плавающей точкой (float и double);
%e – ввод - вывод числа в экспоненциальной форме (double и float);
%c – ввод - вывод символа (char);
%l – ввод – вывод длинного значения (long);
и другие.
При вводе и выводе необходимо строгое соответствие типа вводимого данного управляющему символу формата.
Пример форматированного ввода и вывода.
#include <stdio.h>
void main(void)
{
int my_int;
float my_float;
printf("\nВведите целое и дробное число\n");
scanf ("%d", &my_int);
scanf ("%f", &my_float);
printf ("%d %f", my_int, my_float);
}
При запуске программы она выведет на экран строку – приглашение ко вводу данных, затем при выполнении каждого scanf будет ожидать ввода данных. Пользователь должен ввести требуемое количество данных, отделяя их друг от друга пробелами или нажатием клавиши Enter. При завершении ввода данные тут же будут выведены на экран самым примитивным образом. Так, если ввести целое 5 и дробное 9.9, то строка вывода будет иметь вид:
5 9.900000
Поскольку при вводе данного функция scanf находится в состоянии ожидания ввода, рекомендуется каждый ввод предварять строкой, выводящей на экран приглашение для ввода данного, в котором пользователю подробно объясняют, что и как он должен сделать, чтобы правильно ввести данные. Этот простой прием существенно улучшит интерфейс любой программы.
При выводе данных для улучшения вывода рекомендуется использовать некоторые приемы.
1. Управляющие символы потока, например:
\n для перевода строки при выводе;
\t для выполнения табуляции.
2. Произвольный текст в форматной строке для приглашения на ввод данного и для пояснений при выводе, например, функция вывода может быть записана так:
printf ("Целое = %d, Вещественное = %f\n", my_int, my_float);
Пробелы в строке текста являются значащими. Теперь, если ввести целое 5 и дробное 9.9, то строка вывода будет иметь вид:
Целое = 5, Вещественное = 9.900000
3. Модификаторы форматов. Они используются для оформления вывода. По умолчанию (без модификаторов) данные выводятся в поле минимальной ширины с точностью 6 знаков после запятой, число прижимается к правому краю поля. Этим выводом можно управлять.
а) Ширина поля, это строка цифр, определяющая наименьший размер поля вывода (позиционирование). Если число не входит в поле, игнорируется.
б) Точность вывода, это две цифры, определяющие общий размер поля вывода и число знаков после запятой. Используется для вещественных чисел.
В примерах обозначим знаком пробелы, которые будут в строке вывода.
printf ("Целое = %4d, Вещественное = %5.2f\n", my_int, my_float);
Если ввести значения 10 и 2.3, то строка вывода будет иметь вид:
Целое = 10, Вещественное = 9.90
Если ввести значения 19951 и 12.9999, то строка вывода будет иметь вид:
Целое = 19951, Вещественное = 13.00
Можно сделать вывод, что число округляется.
в) Знак минус используется для выравнивания числа влево внутри поля вывода.
printf ("Целое = %–4d, Вещественное = %–5.2f\n", my_int, my_float);
Если ввести значения 2 и 2.36666, то строка вывода будет иметь вид:
Целое = 2, Вещественное = 2.37
Если ввести значения –1999 и 12.9999, то строка вывода будет иметь вид:
Целое = –1999, Вещественное = 13.00
Пример использования форматированного ввода-вывода.
#include <stdio.h>
#define STR "Программа" // Для иллюстрации вывода строк.
void main (void)
{
// Вывод целого числа 336
printf ("%d\n", 336); // 336
printf ("%2d\n", 336); // 336, формат 2d игнорируется
printf ("%8d\n", 336); // 336 // ширина поля 8
printf ("%-8d\n", 336); // 336 // прижато влево
printf("\n"); // Пропуск строки при выводе
// Вывод вещественного числа 12.345
printf ("%f\n", 12.345); // 12.345000
printf ("%e\n", 12.345); // 1.234500е+01
printf ("%10.1f\n", 12.345); // 12.3
printf ("%–12.1f\n", 12.345); // 12.3
printf("\n");
// Вывод строки символов по формату s
printf ("%s\n", STR); // Программа
printf ("%12s\n", STR); // Программа
printf ("%12.5s\n", STR); // Прогр
printf ("%-12.5s\n", STR); // Прогр
printf("\n");
}
1.2.10. Управляющие конструкции языка Си++
Определение. Оператор, это предложение, описывающее одно действие по обработке данных или действия программы на очередном шаге ее исполнения.
Назначение операторов:
- Преобразование данных.
- Управление ходом выполнения программы на очередном шаге ее выполнения.
1.2.10.1. Классификация операторов.
К первой группе относятся операторы преобразования данных. Строго говоря, это выражение. Оператора присваивания в языке Си++ нет, его заменяет выражение, в составе которого есть операция присваивания.
Примеры операторов изменения данных:
y + 4; // Выражение.
x = y+4; // Выражение присваивания.
x ++; //Выражение – оператор.
Операция присваивания правоассоциативна, поэтому допускается запись цепочек присваиваний, например:
x = y =z = 1; // Каждая переменная будет равна 1.
Еще один вид операторов, который можно отнести к операторам преобразования данных, это оператор обращения к функции (вызов функции). Его операцией являются круглые скобки, а операндами имя_функции и список_парамеров_функции:
scanf ("%d%f", &a, &b);
printf ("a = %d, b = %f", &a, &b);
При выполнении вызова функции scanf значения переменных a, b будут изменены. При выполнении вызова функции printf выполняются действия по преобразованию данных из внутреннего представления в текстовое, пригодное для вывода на экран.
Ко второй, значительно большей группе, относятся операторы управления, предназначенные для управления работой программы.
Обычно операторы программы выполняются в том порядке, в котором записаны. Поскольку каждый оператор выполняет одно действие по обработке данных, тем самым, управляя работой компьютера, то порядок выполнения выражений называют потоком управления. Поток управления редко бывает линейным. Изменить направление потока управления позволяют операторы управления, перечисленные в таблице1.2.10.1.1.
Название оператора
|
Ключевое слово
|
Составной оператор
|
{…}
|
Условный оператор
|
if
|
Оператор цикла с проверкой условия до выполнения
|
while
|
Оператор цикла с проверкой условия после выполнения
|
do … while
|
Оператор цикла типа «прогрессия»
|
for
|
Оператор прерывания
|
break
|
Оператор продолжения
|
continue
|
Оператор переключатель
|
switch
|
Оператор перехода
|
goto
|
Таблица 1.2.10.1.1. Операторы управления Си++
Излагая сведения о каждом из операторов Си++, будем придерживаться следующей схемы:
- назначение;
- синтаксис;
- механизм исполнения;
- пример;
- особенности.
1.2.10.2. Выражение присваивания
Назначение. Изменение значения данного в соответствии с выражением правой части.
Синтаксис.
Имя_переменной = выражение; // Форма 1.
Имя_переменной = (тип) выражение; // Форма 2.
Выполнение. В первую очередь вычисляется выражение правой части, во вторую вычисленное значение присваивается переменной левой части.
Пример.
x = y =0;
x ++;
y = sin ( 2 * PI * x);
y + = 0.2;
Особенности. Если в выражении встречаются операнды разных типов, то при вычислении значения выражения происходит неявное преобразование типов, как правило, к большему типу. При несоответствии типа выражения правой части типу переменной в левой части, происходит приведение типа выражения к типу переменной, при этом неизбежна потеря данных при приведении от большего типа к меньшему.
1.2.10.3. Составной оператор
К составным операторам относятся собственно составные операторы и блоки. В обоих случаях это последовательность операторов, заключенная в фигурные скобки. Блок отличается тем, что в его состав входят описания каких-либо объектов программы.
Пример.
{// Это составной оператор.
n ++;
S += n;
}
|
{// Это блок.
int n = 0;
n ++;
S += n;
}
|
Блоки, чаще всего, используются в качестве тела функции.
Составной оператор используется в любом случае, когда несколько операторов следует объединить в один. Это необходимо в условных операторах и операторах цикла, согласно синтаксису которых, исполнимым является только один оператор. Чаще всего, составной оператор формирует ветвь условного оператора или тело цикла в операторах цикла.
1.2.10.4. Условный оператор (if)
Назначение. Выбор одного из двух возможных путей исполнения алгоритма (программы) в зависимости от условий, сложившихся при выполнении.
Для проверки условия формулируется некоторое утверждение, которое может быть истинным или ложным. Это логическое выражение, которое может принять одно из значений True (истина) либо False (ложь). В языке Си++ они имеют форму выражений целочисленного типа, принимая значения 1 либо 0 (точнее, «не 0» либо «0»).
Синтаксис. Синтаксис условного оператора имеет две формы.
Первая форма сокращенная.
if (Логическое_выражение)
оператор;
Здесь «Логическое_выражение» – любое, тип которого int (значение может быть равно 0 или отлично от 0), оператор – это один или несколько операторов, в общем случае составной оператор. Логическое выражение записывается в скобках. Логическая схема оператора приведена на рис. 1.2.10.4.1.
Рис. 1.2.10.4.1. Логическая схема оператора if с одной веткой.
Вторая форма полная.
if (Логическое_выражение)
оператор1;
else
оператор2;
Здесь «Логическое_выражение» имеет тот же смысл, что и ранее, а оператор1 или оператор2, это в общем случае блок. Логическое выражение записывается в скобках. Логическая схема оператора приведена на рис. 1.2.10.4.2.
Рис. 1.2.10.4.2. Логическая схема оператора if с двумя ветвями.
Выполнение.
1. Вычисляется значение логического выражения.
2. Если оно не равно 0, выполняется оператор 1, а если оно равно 0, выполняется оператор 2 (или ничего в первой форме).
В общем случае «оператор» является составным, то есть это последовательность операторов, взятая в фигурные скобки.
В общем случае «Логическое_выражение» – это сколь угодно сложное выражение, вычисляющее целочисленное значение. Поскольку логического типа данных нет, его заменяет значение типа int. Истинным считается значение, отличное от нуля.
Пример 1. Первая форма условного оператора.
Стоимость равна произведению цены на количество. Если есть скидка, то стоимость уменьшается на величину discount. Вычислить стоимость.
pay = cost * count; //Общая формула.
if (discount != 0) //Если есть скидка, то стоимость уменьшается.
pay = pay*discount / 100;
printf (“Стоимость = %6.2f\n”, pay); // Вывод pay в любом случае.
Пример 2. Вторая форма условного оператора.
Оплата труда работника, это произведение количества отработанных часов на стоимость часа. Если отработано более 40 часов, то за каждый час работодатель платит в полтора раза больше.
if (hour < 40)
pay = rate*hour; // Обычная оплата, hour < 40.
else
pay = rate*40+(hour – 40)*rate*1.5; // Повышенная оплата, hour >= 40.
printf (“К оплате %6.2f рублей.\n”, pay); // Печать одинакова.
Пример 3. Использование блоков в составе условного оператора.
Пусть необходимо вывести на экран не только значение суммы к оплате, но и число оплаченных часов, тогда для каждой ветви нужна своя собственная печать.
if (hour < 40)
{
pay = rate * hour; // Обычная оплата.
printf (“Оплачено %d часов, к оплате %6.2f руб.\n”, hour, pay);
}
else
{
pay = rate*40+(hour – 40)*rate*1.5; // Повышенная оплата.
printf (“Оплачено %d часов, %d сверхурочно.\n”, hour, hour – 40);
printf (“К оплате %6.2f руб.\n”, pay);
}
Пример 4. Вложенный условный оператор.
В состав операторов, формирующих содержание условного оператора, могут входить любые операторы, в том числе условные. В этом случае говорят о вложенном условном операторе. Каждая из ветвей, в свою очередь, может содержать в своем составе условный оператор или несколько. Число уровней вложения не ограничено, однако чем их больше, тем сложнее восприятие текста. Правила организации подобных структур требуют, чтобы любой внутренний уровень полностью принадлежал одной из ветвей внешнего уровня.
Структурная схема вложенного условного оператора приведена на рис. 1.2.10.4.3.
Рис. 1.2.10.4.3. Структурная схема вложенного условного оператора.
Примеров задач, в которых необходимо использование вложенного условного оператора, достаточно много. Это любая задача, где для выбора решения следует рассмотреть более одного условия.
Например, при стрельбе по мишени известна координата попадания пули в мишень (x, y). Количество очков зависит от того, насколько близко пуля попала к центру мишени, и изменяется от до 10 до 0. Радиусы колец известны, и равны, например, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20. Небольшая иллюстрация приведена на рис. 1.2.10.4.4.
Рис. 1.2.10.4.4. Постановка задачи
Условие получения 10-ти очков, это условие попадания точки в круг радиуса r = 2: . Условие получения 9-ти очков, это условие попадания точки в кольцо с внутренним радиусом r1 = 2 и внешним r2 = 4: , и так далее. Если первое условие выполнено, результат уже получен, если же нет, следует проверить следующее, и так далее.
Приведем фрагменты текста программы, которая решает эту задачу.
void main(void)
{
float x, y; // Координаты точки попадания.
int Ball; // Число очков за выстрел.
// Предполагается, что к моменту проверки условия координаты точки попадания
// известны.
float Shot; // Рабочая переменная Shot (выстрел)
Shot = x*x + y*y; // используется для сокращения записи.
if (Shot <= 4) // Во внутреннем круге.
Ball = 10; // Блок 1
else
if (Shot <= 16) // Блок 2
Ball = 9; // Блок 2_1
else
if (Shot <= 36) // Блок 2_2
Ball = 8; // Блок 2_2_1
else if (Shot <= 64) // Блок 2_2_2
Ball = 7; // Блок 2_2_2_1 else
if (Shot <= 100) //…
Ball = 6;
else
if ( Shot <= 144)
Ball = 5;
else
if ( Shot <= 186)
Ball = 4;
else
if ( Shot <= 256)
Ball = 3;
else
if (Shot <=324)
Ball = 2;
if ( Shot <= 400)
Ball =1;
else
Ball = 0;
// Обработка результатов и вывод.
}
Обратите внимание, что условие проверки попадания вне всех колец мишени не нуждается в проверке, так как получается автоматически.
Вместо одного вложенного условного оператора можно использовать несколько операторов проверки условия, как в следующем фрагменте:
if (Shot <= 4)
Ball = 10;
if (4 < Shot && Shot <= 16)
Ball = 9;
if (16 < Shot && Shot <= 64)
Ball = 8;
// И так далее.
if (Shot >20)
Ball = 0;
В этом случае сложность переносится в запись условий.
Особенности.
1. Особенности записи логических выражений.
Самое простое логическое выражение содержит только операнды (в общем случае выражения) и знаки отношений, например, x +3 > 0, x < 0.5*y, Angle = = 90, и им подобные. Естественная логика этих выражений не требует пояснений. Тип значения, возвращаемого таким выражением, будет int.
Запись операции отношения корректна, когда сравниваются значения одинаковых типов. Если операнды разного типа, то перед выполнением сравнения компилятор выполнит приведение типов от меньшего к большему. Например, при вычислении отношения My_float >= My_Int значение My_Int будет временно приведено к вещественному типу, и результат операции может быть отличным от ожидаемого. Если такого рода сравнения необходимы, следует четко обозначить намерения программиста применением явного преобразование типа, например My_float >= (float) My_Int или (int) My_float >= My_Int.
Для записи более сложных логических выражений используются логические операции:
&& – логическое «и»;
|| – логическое «или»;
! – логическое отрицание «не».
Например, для обозначения категории «подростковый возраст» нужна условная градация, скажем, от 12 до 17 лет. Если переменная Age обозначает возраст некоего круга людей, то подростками из них будут только те, для которых справедливо утверждение Age >= 12 && Age <=17. Логическое выражение будет истинно, только когда оба условия выполняются одновременно. Соответственно, если Age < 12 || Age > 17, то человек не подросток (это ребенок, если Age < 12, или взрослый человек, если Age > 17). Логическое выражение будет истинно, когда хотя бы одно или оба условия выполнены. Для того, чтобы проиллюстрировать применение операции отрицания, запишем условие «не подросток» инверсией выражения «подростковый возраст»: ! (Age >= 12 && Age <=17).
Приоритет логических операций и порядок их выполнения в случае равных приоритетов приведены в таблице 1.2.6.3.1.
Наиболее распространенные ошибки записи логических выражений.
- Знаки логических операций похожи на знаки операций поразрядного сравнения & и |. Выполнение этих операций происходит различным образом, поэтому не всегда результат поразрядного сложения равен истинной дизъюнкции операндов, а результат поразрядного умножения равен истинной конъюнкции (хотя иногда равен).
- Знак операции = отличен от знака = =. Знаки операций = и = = похожи внешним образом, но радикально отличаются по механизму выполнения. Каждый программист хотя бы раз допустил ошибку вида:
if ( Key = 27)
{
printf (“Завершение работы.\n”);
return;
}
else
printf (“Продолжение работы.\n”);
Здесь по смыслу алгоритма значение переменной Key должно определить, нужно ли завершить работу программы, или нужно продолжить. Однако в этом тексте, независимо от первоначального значения Key, будет выполнено присваивание Key = 27, и значение выражения станет равно 1. Значит, результат проверки значения выражения равен 1, всегда выполняется первая ветка, и программа благополучно всегда завершит свою работу.
- Пропуск знаков логических операций. Математическая запись 3 < x <6 должна быть записана на Си++ обязательно с использованием логической операции &&: if ( – 3 < x && x < 6)…, хотя компилятор согласится с текстом вида if ( – 3 < x < 6)... При вычислении выражения – 3 < x < 6 сначала вычисляется ( – 3 < x ). Это выражение имеет логическое значение (0 или 1). Затем полученное логическое значение участвует в вычислении выражения (– 3 < x) <6, результат которого всегда истинен.
- Операции сравнения для вещественных типов. Операции сравнения могут применяться к данным любого из базовых типов Си++. Для данных целых типов и символьного точное соответствие возможно. Для данных вещественных типов всегда существует ошибка представления данных, которая может накапливаться. Поэтому проверки на точное равенство вещественных чисел следует избегать.
Пример.
#include <stdio.h>
void main(void)
{
float x=1./3.;
float y;
y = x+x+x;
if (y= =3*x)
printf("Равны\n");
else
printf("Не равны\n");
}
Казалось бы, y должен быть равен 3*х, но в силу ошибки представления данных вещественного типа, этого не произойдет.
Проверка вещественных чисел выполняется на приближенное равенство с использованием приближенного значения модуля разности сравниваемых значений (оно примерно равно нулю):
(fabs (y – 3*x) < 0.001)
Некоторые примеры записи логических выражений приведены в таблице 1.2.10.4.1.
|
Условие
|
Математическая формулировка
|
Синтаксис записи на Си++
|
1.
|
Число Х принадлежит отрезку [–2,+2]
|
–2 <= x <= +2
Или |x| <= 2
|
–2.<=x && x<=2.
Или fabs (x) <=2/
|
2.
|
Число Х принадлежит отрезку [–2,–4] или отрезку [+2,+4]
|
–4 <= x <= –2 ,
+2 <= x <=+4
|
–4. <= x && x <= 2. ||
2. <= x && x <= 4.
|
3.
|
Точка с координатами (x,y) находится в первой четверти.
|

|
x > 0 && y>0
|
4.
|
Точка с координатами (x,y) находится в первой или в третьей четверти
|
, 
|
x > 0 && y>0 || x < 0 && y < 0
|
5.
|
Числа a, b, c одновременно четны
|
Каждое число делится на 2 без остатка.
|
a%2 == 0 && b%2 == 0 &&
c%2 ==0
|
6.
|
Хотя бы одно из чисел a, b, c четно
|
Одно, или два, или все три числа делятся на 2 без остатка
|
a%2 == 0 || b%2 == 0 || c%2 ==0
|
7.
|
Символ является знаком препинания.
|
|
char d; // объявление переменной.
d == '.' || d == ',' || d == '?' || d == '!'
|
8.
|
Символ является буквой латинского алфавита.
|
|
char d; // объявление переменной.
d >= 'A' && d <= 'Z' ||
d >= 'a' && d <= 'z'
|
9.
|
Точка с координатами (x,y) принадлежит указанной области
|
Во второй четверти

В четвертой четверти

|
x>–1 && x<0 && y>0 && y<1 ||
x>0 && x<1 && y<0 && y>-1
|
10.
|
Точка с координатами (x,y) находится выше прямой y = a·x + b.
|
При подстановке в уравнение прямой y1= a·x+b:
y1>y, то выше,
иначе ниже.
|
// Значения a, b, x, y известны.
a*x+b>y
|
1.2.10.5.Оператор цикла while
Назначение. Организация многократного повторения произвольного фрагмента программы.
Синтаксис.
while (Логическое_выражение)
{
тело цикла;
}
Здесь тело цикла – один или несколько операторов, в общем случае составной оператор. «Логическое_выражение», это выражение условия завершения цикла. Как правило, выражение, значение которого может быть равно 0 («ложно») или отлично от нуля («истинно»). Логическая схема оператора приведена на рис. 1.2.10.5.1.
Рис. 1.2.10.5.1. Логическая схема оператора цикла while
Выполнение. Тело цикла выполняется, когда Логическое_выражение имеет значение «истинно» (отлично от 0). Когда Логическое_выражение «ложно», управление передается оператору, стоящему за циклом.
Пример. Найдем сумму n чисел натурального ряда. . Параметр цикла i ∈ [1,n], Δi = 1. Тело цикла S = S + i.
void main (void)
{
int n, Sum = 0;
int i;
printf ("\nВведите число элементов прогрессии \n");
scanf ("%d", &n);
i = 1;
while (i <= n)
{
Sum += i; // Очередное сложение.
i++; // Увеличение слагаемого.
}
printf("Сумма арифметической прогрессии %d", Sum);
}
Поскольку значение суммы не зависит от порядка суммирования, то в стиле Си++ алгоритм запишется так:
i = n;
Sum = 0;
while (i) // Пока i отлично от 0.
Sum += i – –;
Особенности. Проверка условия происходит до выполнения оператора, значит, возможно (для заведомо ложного выражения), что тело цикла не будет выполнено ни разу.
1.2.10.6. Оператор цикла do...while
Назначение. Организация многократного повторения произвольного фрагмента программы.
Синтаксис.
do
{
тело цикла;
}
while (Логическое_выражение);
Здесь тело цикла, это в общем случае составной оператор. Логическое_выражение – это выражение условия. Как правило, логическое выражение или арифметическое, значение которого может быть равно 0 (ложно) или нет (истинно). Логическая схема оператора приведена на рис. 1.2.10.6.1.
Рис. 1.2.10.6.1. Логическая схема оператора цикла do…while
Выполнение. Тело цикла (в общем случае блок) выполняется многократно, пока выражение не равно 0. Как только выражение ложно (т.е. = 0), цикл заканчивается, управление передается следующему по порядку оператору.
Пример. Решим ту же задачу с использованием оператора цикла do, который запишем в стиле Си++.
void main (void)
{
int n, Sum = 0;
int i;
printf ("\nВведите число элементов прогрессии \n");
scanf ("%d", &n);
i = n;
do
Sum += i– –;
while (i); // Пока i отлично от 0.
printf ("Сумма арифметической прогрессии %d", Sum);
}
Особенности. Проверка условия происходит после выполнения тела цикла. Значит, как бы ни было задано «Логическое_выражение», оператор тела цикла выполнится не менее чем один раз.
1.2.10.7. Оператор цикла типа «прогрессия» for
Назначение. Организация многократного повторения произвольного фрагмента программы. Как правило, этот цикл используется, когда число повторений его известно заранее, и явно есть переменная величина, которая, изменяясь, составляет прогрессию. Она, как правило, является параметром цикла.
Синтаксис.
for (выражение1; выражение2; выражение3)
тело цикла;
Логическая схема оператора приведена на рис. 1.2.10.7.1.
Рис. 1.2.10.6.1. Логическая схема оператора цикла for
Здесь параметр цикла составляет прогрессию, и явно присутствует в записи заголовка цикла (так называется строка for(…)). «выражение1» задает начальное значение параметра цикла, «выражение2» задает условие завершения выполнения цикла, и является логическим, «выражение3» задает приращение параметра цикла. Тело цикла, это произвольная последовательность операторов, как правило, составной оператор. Первое и третье выражения могут состоять из нескольких, отделенных друг от друга запятой.
Выполнение. Перед входом в цикл, то есть до первого выполнения тела цикла однократно выполняется выражение1. Тело цикла выполняется многократно, пока «выражение2» (условие) не равно 0. Как только его значение становится равно 0, управление передается следующему по порядку оператору. Проверка условия завершения цикла происходит до выполнения тела цикла.
Пример. Покажем, как та же задача решается с использованием цикла for.
void main (void)
{
int n, Sum = 0;
int i;
printf ("\nВведите число элементов прогрессии \n");
scanf ("%d", &n);
for ( i = 1; i <= n; i ++)
Sum += i;
printf("Сумма арифметической прогрессии %d", Sum);
}
Особенности.
1. Управляющая переменная цикла for не обязательно целого типа, она может быть вещественной или символьной. Приведем пример цикла, вычисляющего сумму геометрической прогрессии ., где i изменяется по закону i = i * 1,1.
void main (void)
{
float Sum = 1, i;
for (i = 1.; i <= 2.; i *= 1.1)
Sum += i;
printf("\nСумма геометрической прогрессии %f", Sum);
}
2. Любое из трех выражений, любые два, или все могут отсутствовать, но разделяющие из символы «;» опускать нельзя. Если «выражение2» отсутствует, то считается, что оно истинно, и цикл превращается в бесконечный, для выхода из которого необходимы специальные средства. Бессмысленно использовать несколько «выражений2», так как управление выполняется по первому условию.
3. Оператор цикла for удобен тем, что интегрирует в заголовке описание всего процесса управления. Тело цикла, в общем случае, блок. Поскольку начальных присваиваний и выражений приращения может быть несколько, весь цикл, вместе с телом, может быть записан одной строкой. Например, тот же цикл в стиле Си++ может выглядеть так:
for ( Sum = 0, i = 1; i <= n; Sum +=i ++);
Здесь два начальных присваивания, и тело цикла записано в его заголовке.
1.2.10.8. Правила организации циклических алгоритмов
Циклические алгоритмы разделяются на две группы:
- Арифметический цикл (управляемый счетчиком). Как правило, повторяется заранее известное число раз. Например, спортсмен должен пробежать 10 кругов или 40 км.
- Итерационный (управляемый событием). Как правило, число повторений заранее неизвестно. Например, спортсмен должен бежать, пока не устанет, или пока суммарный путь пробега не составит 42 км.
Программа должна организовать правильное управление процессом выполнения циклического алгоритма. Для управления в программе используется некая величина, которая называется «параметр цикла» (управляющая переменная). Это одна из переменных программы, которая, как правило, изменяется в теле цикла, определяет число повторений цикла и позволяет завершить его работу.
При выполнении цикла всегда должны быть пройдены следующие этапы.
- Подготовка цикла: включает действия, которые не относятся непосредственно к логической схеме цикла, но позволяют правильно его выполнить. Как правило, на этом этапе выполняется присваивание начальных значений переменным, в том числе параметру цикла.
- Точка входа в цикл: момент передачи управления первому оператору тела цикла.
- Итерация: очередное выполнение тела цикла, то есть фрагмента, который должен быть повторен многократно. Как правило, включает в себя изменение параметра цикла.
- Точка проверки условия: момент проверки условия, при котором решается, делать ли новую итерацию, или перейти к оператору, стоящему за циклом. Как правило, в проверке условия явно или нет, присутствует параметр цикла.
- Выход из цикла: передача управления оператору, стоящему за циклом.
Не всегда эти составляющие присутствуют явным образом.
В Си++ существуют три вида операторов цикла, которые служат инструментом для организации циклических процессов.
1. while…do:
// Подготовка цикла
while (условие)
{ //Количество повторений любое.
тело цикла
}
2. do…while:
// Подготовка цикла
do
{ //Количество повторений любое.
тело цикла
}
while (условие);
3. for:
for (объявление параметра цикла)
{ //Количество повторений фиксировано.
тело цикла
}
С помощью каждого из этих операторов можно организовать циклический алгоритм любого типа. Вариантов задач, в которых необходимо использование циклических алгоритмов, довольно много, например, вычисление таблиц значений функций, накопление суммы, счет количества, учет предыдущего значения и так далее.
При проектировании цикла необходимо решать две задачи:
1. Разработка потока управления. Здесь наиболее важный шаг, это выбор параметра цикла. Для параметра цикла должно быть известно:
- Каково условие завершения цикла.
- Каково начальное значение параметра.
- Как обновляется параметр цикла.
2. Планирование действий внутри цикла. Здесь важно решить, что представляет собой отдельная итерация, и точно определить:
- Как инициализируется повторяющийся процесс.
- Какие действия в него входят.
- Как он обновляется.
Приведем пример проектирования циклического алгоритма, в котором использованы различные операторы цикла для решения одной и той же задачи. Условие задачи: снаряд выпущен под углом λ к горизонту со скоростью V. Траектория полета имеет вид, как на рис. 1.2.10.8.1.
Рис. 1.2.10.8.1. Постановка задачи
Требуется определить высоту и дальность полета в течение промежутка времени от t = 1 сек. до t = 10 сек с интервалом, равным 1 сек.
Управляющей переменной является время, t ∈ [1, 10], шаг Δt = 1. Так мы определяем изменение параметра цикла и условие выхода. Содержанием тела цикла является вычисление очередного значения и вывод на печать. Число повторений заранее известно, и равно 10-ти, значит, цикл арифметического типа. Арифметический цикл можно организовать, используя любой из операторов цикла Си++, что и показано в полном тексте программы.
#include <stdio.h>
#include <math.h>
#define G 9.8 // Константа тяготения.
void main(void)
{
// Переменные, которые являются начальными значениями.
float V; // Скорость.
int A_Grad; // Угол в градусах.
// Переменные, участвующие в вычислениях.
float Alpha; // Угол.
float t; // Время полета.
float Sy, Sx; // Высота и дальность.
printf ("Введите скорость и угол в градусах\n");
scanf ("%f%d", &V, &A_Grad);
Alpha = (float) A_Grad * M_PI/360.;
// Арифметический цикл. Управляющая переменная t = [1.0; 10.0], Δt= 1.
// 1. Оператор for.
printf ("--------------------------------------\n");
printf ("--Время--Высота--Дальность\n"); // Подготовка цикла
printf ("--------------------------------------\n");
for (t = 1.;t <= 10.; t += 1.)
{
Sx = V*t*cos (Alpha);
Sy = V*t*sin (Alpha) – 0.5*G*t*t;
printf ("%6.2f %6.2f %6.2f\n", t, Sy, Sx);
}
// 2. Оператор while.
printf ("--------------------------------------\n");
printf ("--Время--Высота--Дальность\n"); // В подготовке цикла
printf ("--------------------------------------\n"); // основное t = 1
t = 1.;
while (t <= 10.)
{
Sx = V*t*cos (Alpha);
Sy = V*t*sin (Alpha) – 0.5*G*t*t;
printf ("%6.2f %6.2f %6.2f\n", t, Sy, Sx);
t+=1.;
}
// 3. Оператор do.
printf ("--------------------------------------\n");
printf ("--Время--Высота--Дальность\n");
printf ("--------------------------------------\n"); // Подготовка цикла
t = 1.;
do
{
Sx = V*t*cos (Alpha);
Sy = V*t*sin (Alpha) – 0.5*G*t*t;
printf ("%6.2f %6.2f %6.2f\n", t, Sy, Sx);
t+=1.;
}
while (t <= 10.);
}
В этом примере алгоритм приведен в трех вариантах, чтобы показать отличия в организации циклических алгоритмов при использовании разных операторов цикла. Решение задачи выявляет одну ее особенность. Приведем одну из таблиц решения, полученную при скорости 100 м./сек. и угле, равном 30°.
--------------------------------------
--Время--Высота--Дальность
--------------------------------------
1.00 20.98 96.59
2.00 32.16 193.19
3.00 33.55 289.78
4.00 25.13 386.37
5.00 6.91 482.96
6.00 -21.11 579.56
7.00 -58.93 676.15
8.00 -106.54 772.74
9.00 -163.96 869.33
10.00 -231.18 965.93
Как видим, результат решения очень сильно зависит от входных данных. Наибольшая дальность полета может быть достигнута за время, меньшее, чем 10 секунд, как было объявлено в постановке задачи. При этом координата, определяющая высоту, становится отрицательной, как будто снаряд пробил землю и продолжает лететь под землей. Этот факт требует улучшения постановки задачи, а именно, нужно отслеживать траекторию полета пока снаряд находится в движении, то есть пока его вертикальная координата больше нуля.
Следовательно, от арифметического цикла следует отказаться, и выбрать итерационный, в котором условием завершения вычислений будет условие «пока снаряд находится в полете», то есть его вертикальная координата больше 0. Итерационный цикл также можно организовать с использованием любого из операторов цикла.
Приведем не полный текст программы, а только основную его часть, отвечающую за организацию циклов. Условие завершения цикла
V*t*sin Alpha) – 0.5*G*t*t > 0
управляет выполнением циклического алгоритма независимо от используемого оператора цикла посредством параметра цикла t, входящего в запись условия.
//1. Оператор for.
printf ("--------------------------------------\n");
printf ("--Время--Высота--Дальность\n");
printf ("--------------------------------------\n");
for (t = 1.;V*t*sin (Alpha) – 0.5*G*t*t > 0.; t += 1.)
{
Sx = V*t*cos (Alpha);
Sy = V*t*sin (Alpha) – 0.5*G*t*t;
printf ("%6.2f %6.2f %6.2f\n", t, Sy, Sx);
}
// 2. Оператор while.
printf ("--------------------------------------\n");
printf ("--Время--Высота--Дальность\n");
printf ("--------------------------------------\n");
t = 1.;
while (V*t*sin (Alpha) – 0.5*G*t*t > 0.)
{
Sx = V*t*cos (Alpha);
Sy = V*t*sin (Alpha) – 0.5*G*t*t;
printf ("%6.2f %6.2f %6.2f\n", t, Sy, Sx);
t += 1.;
}
// 3. Оператор do.
printf ("--------------------------------------\n");
printf ("--Время--Высота--Дальность\n");
printf ("--------------------------------------\n");
t = 1.;
do
{
Sx = V*t*cos (Alpha);
Sy = V*t*sin (Alpha) – 0.5*G*t*t;
printf ("%6.2f %6.2f %6.2f\n", t, Sy, Sx);
t += 1.;
}
while (V*t*sin (Alpha) – 0.5*G*t*t > 0.);
При выполнении этой программы процесс завершается по событию, число строк результирующей таблицы зависит от входных данных, и при тех же начальных значениях таблица решения будет такой:
--------------------------------------
--Время--Высота--Дальность
--------------------------------------
1.00 20.98 96.59
2.00 32.16 193.19
3.00 33.55 289.78
4.00 25.13 386.37
5.00 6.91 482.96
Изменим постановку задачи, чтобы получить чисто итерационный процесс. Например, определить время полета и его дальность при заданных начальных значениях скорости и угла. В этой постановке задачи итоговыми значениями являются t и Sx, но для проверки условия завершения нужно вычислять значение вертикальной координаты. Переменная Sy не нужна, так как формула проверки условия записана в виде логического выражения. Вывод заголовка не входит в подготовку цикла, а вывод на печать вынесен за тело цикла.
//1. Оператор for.
for (t = 1.; V*t*sin (Alpha) – 0.5*G*t*t > 0.;t+=1.)
Sx = V*t*cos (Alpha);
printf ("--Время--Дальность\n");
printf ("%6.2f %6.2f \n", t, Sx);
// 2. Оператор while.
t = 1.;
while (V*t*sin (Alpha) – 0.5*G*t*t > 0.)
{
Sx = V*t*cos (Alpha);
t += 1.;
}
printf ("--Время--Дальность\n");
printf ("%6.2f %6.2f \n", t, Sx);
// 3. Оператор do.
t = 1.;
do
{
Sx = V*t*cos (Alpha);
t += 1.;
}
while (V*t*sin (Alpha) – 0.5*G*t*t > 0.);
printf ("--Время--Дальность\n");
printf ("%6.2f %6.2f \n",t,Sx);
Еще более интересной задачей является задача определения попадания в цель. Если известны координата цели и ее размер, то после выполнения цикла значение Sx можно сравнить с координатой цели, например, пусть координата центра мишени Sm, а ее линейный размер L, тогда условие попадания запишется:
if ( Sx < Sm + 0.5*L && Sx > Sm – 0.5*L)
printf ("Цель поражена\n");
else
if (Sx < Sm – 0.5*L)
printf ("Недолет\n");
else
printf ("Перелет\n");
Надо заметить, что и в последнем случае задача решена некорректно. Значение t выбрано дискретным, и изменение t равно 1 сек. Следовательно, точность вычисления неизвестна, но весьма невелика.
Еще более интересной задачей является задача подбора входных данных t и V таким образом, чтобы обеспечить попадание в цель. Это задача моделирования, имеющая более сложное решение.
Сложный цикл.
Моделирование ??
Перехват события клавиатуры.
1.2.10.9. Оператор прерывания break для циклов do, while, for
Назначение. Прекращение выполнения цикла с передачей управления следующему за циклом оператору.
Синтаксис.
break;
Выполнение. Оператор break осуществляет выход из цикла на следующий по порядку за циклом оператор. Используется для прерывания при организации бесконечных циклов, позволяя корректно завершить цикл.
Пример. Найти сумму арифметической прогрессии всех чисел натурального ряда, которая не превышает некоторого наперед заданного значения, например, n.
void main(void)
{
int n, Sum = 0, i; // i – числа натурального ряда.
printf("\nВведите наибольшее значение\n");
scanf ("%d", &n);
for (i = 1; ;i ++) // Бесконечный цикл.
{ // Суммирует числа i.
Sum += i;
if (Sum > n) // Условие завершения.
break; // Условие завершения.
}
printf ("Количество элементов, включенных в сумму %d\n", i);
}
Особенности. В случае вложения циклов оператор break прерывает только непосредственно охватывающий цикл.
Бесконечный цикл?
1.2.10.9. Оператор продолжения continue для циклов do, while, for
Назначение. Переход к следующей итерации тела цикла, без прерывания выполнения цикла. Противоположен break.
Синтаксис.
continue;
Выполнение. В любой точке цикла continue прервет текущую итерацию и перейдет к проверке условия завершения цикла.
Пример. Найти сумму дробей вида , если x ∈ [–1;+1]. При вычислении этой суммы при х = 0 есть особая точка, в которой знаменатель дроби равен 0 и, следовательно, значение слагаемого не определено. Можно организовать два цикла сложения с управлением по x ∈ [–1;–0.9] и x ∈ [0.1;+1], а можно в одном цикле предусмотреть особую точку, и не вычислять очередное слагаемое.
void main (void)
{
float Sum = 0, x;
for (x = –1; x < =1.05; x += 0.1)
{
if (fabs (x) < 0.0001)
continue; // Если в знаменателе 0
Sum += 1 / x;
}
printf ("Сумма = %8.2f\n", Sum);
}
Особенности. Особенностей нет.
1.2.10.10. Оператор выбора switch
Назначение. Организация множественного ветвления, когда при выполнении алгоритма возможно выполнение ветвления на несколько ветвей в зависимости от условий, сложившихся при выполнении программы.
Синтаксис.
switch (Выражение)
{
case значение1:
{
оператор_1;
break;
}
case значение2:
оператор_2;
break;
}
// И так далее
case значениеN:
{
оператор_N;
break;
}
default:
{
оператор_N+1;
}
}
Слово switch, это название оператора. Здесь «выражение», – это целочисленное или символьное выражение, которое может принять одно из нескольких прогнозируемых значений. Каждая метка case связана с константой, в тексте они названы «значение_НОМЕР». Все значения констант должны быть различны. Слово default также обозначает отдельную метку.
Выполнение. Вначале вычисляется значение выражения, затем вычисленное значение последовательно сравнивается с каждой константой «значение_НОМЕР». При первом же совпадении выполняются операторы, помеченные данной меткой. Обычно это составной оператор, который завершается оператором прерывания break, или return, exit(), как показано в описании синтаксиса. В этом случае происходит прерывание с выходом из switch на следующий по порядку оператор. Если в составном операторе нет оператора прерывания, то после выполнения операторов, найденных по совпадению значения, выполняются операторы всех последующих вариантов по порядку до тех пор, пока на будет встречен опертор прерывания или не закончится оператор выбора. Операторы, стоящие за меткой default выполняются тогда, когда значение выражения не совпало ни с одним значением константы «значение_НОМЕР». default может отсутствовать. Логическая схема оператора в этом варианте приведена на рис. 1.2.10.10.1. Она называется альтернативным выбором (селектором). Его особенность в том, что при выборе всегда выполняется только одна ветвь. Логическая схема оператора в другом варианте, когда операторов break нет в записи оператора выбора, приведена на рис. 1.2.10.10.2. Она называется переключателем. Его особенность в том, что при выборе выполняется выбранная ветвь, а за ней все последующие согласно записи оператора.
Рис. 1.2.10.10.1. Логическая схема оператора выбора как альтернативного выбора
Пример 1. Для управления используется символьное значение знака операции. При вычислении S, если sign = '+', то значение x прибавляется к S, если sign = '–', вычитается. Если sign отличен от символов операций '+', или '–', то S = x.
сhar sign;
…// Здесь sign каким-то образом получает значение.
S = 0;
switch (sign)
{
case '+' :
{ S += x; break; }
case '–' :
{ S –= x; break; }
default :
S = x;
}
Пример 2. Пример нахождения суммы N слагаемых вида
S = Sin(x) + Cos(x) + Sin(x) + Cos(x) +…
for (n = 1, S =0 ; n <= N; n++)
{
switch (n%2==0)
{
case 0:
{ S += sin (x); break; }
case 1:
{ S += cos (x); break; }
}
}
Рис. 1.2.10.10.2. Логическая схема оператора выбора в варианте переключателя
Пример 3. Если в этом же фрагменте убрать оператор прерывания, то будет найдена сумма N слагаемых вида:
S = Cos(x) + Sin(x) + Cos(x) + Cos(x) + Sin(x) + Cos(x)…
for (n = 1; n <= N; n++)
{
switch (n%2==0)
{
case 0:
S += sin (x);
case 1:
S += cos (x);
}
}
1.2.11. Введение в механизм функций
Си++ – процедурно-ориентированный язык. Принципы программирования Си++ основаны на понятии функции.
Как правило, прикладная программа на Си++ содержит несколько файлов кода, где каждый файл, это совокупность функций. Функции объединяются в модули, модули в проект. В состав языка входят также библиотеки функций, которые содержат функции стандартной обработки данных.
1.2.11.1. Модульное программирование
Определение. Модуль – отдельный файл, в котором группируются функции и связанные с ними данные. Решает обособленную задачу обработки данных.
Для начинающих программистов рекомендуется использование модульного стиля программирования, достоинства которого очевидны.
1. Алгоритмы отделены от данных. Данные, как правило, имеют какую-то структуру. Алгоритмы обработки данных определяются составом и структурой данных.
2. Высокая степень абстрагирования проекта. Достигается использованием функциональной декомпозиции.
Модули отлаживаются отдельно друг от друга. Чем больше они независимы друг от друга, тем легче процесс отладки. Функции, входящие в состав каждого модуля, представляют его интерфейс. Для использования модуля достаточно знать только его интерфейс, не вдаваясь в подробности реализации.
Функциональная декомпозиция, это метод разработки программ, при котором задача разбивается на ряд легко решаемых подзадач, решения которых в совокупном виде дают решение исходной задачи в целом.
Проектирование приложения строится от абстрактного описания основной задачи (высший уровень абстракции). Основная задача может быть разбита на ряд более простых подзадач (второй уровень абстракции), каждая из которых, в свою очередь, на ряд еще более простых. Процесс детализации заканчивается, когда очередная подзадача не может быть больше разбита на более простые составляющие, или когда решение очередной задачи становится очевидным.
В процессе детализации создается иерархические дерево решения задачи, где каждый уровень дерева представляет собой решение более детализированной задачи, чем предшествующий уровень. Иллюстрация приведена на рисунке 1.2.11.1.1.
Рис. 1.2.11.1.1. Блок-схема модульной структуры приложения.
На схеме каждый блок представляет собой программный модуль. Каждый модуль, это законченный алгоритм решения некоторой конкретной задачи.
Процесс кодирования выполняется снизу вверх, от написания и полной отладки кода небольших подзадач с их последующей сборкой на верхнем уровне, при этом каждый модуль безошибочно решает одну задачу. Объем задачи нижнего уровня достаточно небольшой, это одна или две страницы кода (не более 50-ти строк).
1.2.11.2. Назначение и виды функций
Определение. Функция, это самостоятельный именованный алгоритм решения некоторой законченной задачи.
Фактически, функция является одним из конструируемых пользователем типов данных, и, как объект программы:
- имеет имя;
- имеет тип;
- может иметь параметры (аргументы функции).
Назначение функций можно рассматривать двояко. Во-первых, как утилитарное, при котором функция используется только для того, чтобы сократить текст программы. Тогда в виде функции записывается выражение, которое повторяется в тексте программы несколько раз. Во-вторых, как требуют принципы модульного подхода, использование функций позволяет получать хорошо структурированные программы. Разделение на функции позволяет сократить время написания и отладки программ. Использование функций в совместной работе над проектом просто необходимо.
Категории функций в Си++, это библиотечные функции и функции, определенные пользователем.
Функции стандартных библиотек Си++ хранятся в компилированном виде (файлы с расширениями lib), и присоединяются на этапе сборки.
Объявления библиотечных функций содержатся в заголовочных файлах с именами <имя.h>. Чтобы объявление стало доступно программе, в тексте программы записывается директива препроцессора #include <имя.h>. При этом текст заголовочного файла <имя.h>, в котором есть объявление функции, включается в текст программы на этапе препроцессорной обработки. В этом смысле объявление функции, это аналог объявлению переменных.
Подробное описание всех библиотек Си++ можно найти в справочной системе.
1.2.11.3. Создание и использование простой функции.
1.2.11.3.1. Описание функции
Описание функции содержит полное абстрактное описание внешних данных функции и исполняемого ею алгоритма. В языке С++ все функции описываются на одном уровне, вложений не допускается.
Структура простой функции не отличается от структуры функции main, и в общем виде такова:
Отдельные составляющие могут быть опущены, кроме имени и блока тела функции.
Первая строка описания функции называется заголовком функции. Эта строка используется для объявления функции в теле программы, потому что в ней указаны все внешние характеристики функции, а именно:
1. Тип функции. Это тип возвращаемого функцией значения или void для функций, которые не возвращают значения. Типом функции может быть имя базового типа или указатель.
2. Имя функции. Имя main для главной функции программы, или любое, не совпадающее с ключевыми словами и именами других объектов программы.
3. Формальные параметры функции. Это перечисленные через запятую имена аргументов функции вместе с их типами. В описании функции указываются формальные параметры. Как правило, это входные данные для функции, то есть те, которые функция получает извне. Они могут быть и результатом работы функции, но об этом позже. В списке параметров указывается void, если параметров нет. Пустой список параметров означает, что список параметров может быть произвольной длины.
Пример описания простой функции.
// Функция возведения в квадрат описана перед функцией main.
float sqr (float x) // Тип, имя, один параметр.
{
return x*x; // Сразу возвращает значение.
}
// Теперь main может к ней обратиться.
void main(void)
{
float a, b;
a = 2;
b = sqr (a);
printf("\n%f", b); // Здесь а – фактический параметр.
}
Замечания о типе функции
Поскольку функция возвращает значение, она обладает типом. В этом смысле функцию рассматривается как данное конструируемого пользователем типа. Данное должно иметь значение. Для функции значением является то, что она возвращает после выполнения, а это значение выражения, записанного в return.
- Если это простой объект, то тип функции какой-нибудь базовый тип.
- Если это объект сложного конструируемого типа (массив, структура и пр.), то тип функции, это указатель (адрес) объекта.
- Если функция не возвращает значения, ее тип никакой, то есть void, и в теле функции нет return.
- Если тип вообще не указан, это не ошибка, тип функции по умолчанию целый (int).
Например: main главная функция любой программы, начинается void main (void). Это означает, что функция не возвращает значения и не имеет параметров.
Если же заголовок функции main (), то это означает, что тип функции int, а число параметров произвольно. В этом случае в теле main должен быть оператор return, возвращающий значение функции в окружающую среду, например, return 1; или return 0;. Параметры для main, если они есть, передаются как параметры командной строки при вызове программы.
Тело функции
Тело функции содержит:
1. Описания локальных переменных. Их область действия, это тело функции.
2. Описание алгоритма.
3. Возврат в точку вызова. Используется оператор return (2 формы).
Описания объектов в теле функции используются, чтобы объявить локальные переменные функции, то есть те, которыми функция пользуется для решения своих частных задач. Областью действия локальных переменных является тело функции, за пределами которого эти переменные не существуют.
Описание алгоритма не представляет особенностей. Это должен быть общий алгоритм решения некоторой самостоятельной задачи. Он самодостаточен, замкнут в себе, абстрактен. Единственное, что связывает его с внешним миром, это входные данные, приходящие извне, и результат работы алгоритма, который он должен передать в вызывающую функцию (вернуть во внешний мир).
Возврат из функции в точку вызова выполняет оператор return, который передает управление в вызывающую программу, и возвращает одно значение. Может иметь две формы
return Выражение; // Выход из функции с передачей значения.
return; // Выход из функции без передачи значения.
Таким образом, возвращаемое значение может быть только одно. На самом деле это не так, далее мы рассмотрим механизм указателей, который позволит решать задачи передачи данных.
Оператор return в тексте функции может быть не один, если алгоритм функции требует, чтобы вариантов выхода из нее было несколько.
Формальные параметры функции
В описании функции ее внешние данные названы формальные параметры. Это название подчеркивает, что данные описания формальны, абстрактны, то есть не участвуют в реальных действиях, а только описывают взаимосвязь данных в теле функции. Количество формальных параметров функции и их типы могут быть любыми.
1.2.11.3.2. Обращение к функции
Под обращением к функции понимается реальное выполнение ее алгоритма, каждый раз с различными входными данными. Выполняется из любой другой функции программы с использованием механизма вызова функции через операцию обращения.
Обращение к функции зависит от типа функции, и может иметь 2 формы.
Первая форма обращения – оператор-выражение
Если функция возвращает значение, то им является одно значение базового типа или указатель. Оно может быть использовано в выражениях или печати в виде обращения к функции (называется «оператор-выражение»), синтаксис которого:
// При использовании в выражениях:
переменная = имя_функции (фактические параметры);
// В списке вывода при печати:
printf ("форматная_строка ", имя_функции (фактические_параметры));.
Покажем на примере обращения к библиотечной функции sin.
y = sin(x); // Значение функции вычислено и присвоено.
printf("%6.2f", sin (x); // Значение функции напечатано.
sin (x); // Значение функции вычислено, но
// что происходит с вычисленным значением?
Вторая форма обращения – оператор-функция
Если функция не возвращает значения (функция типа void), то обращение к ней выглядит как обычный оператор программы, и имеет специальное название «оператор-функция», синтаксис которого:
имя_функции (фактические параметры);
Оператор-функция, выглядит, как обычный оператор программы. Покажем на примере обращения к библиотечной функции printf.
printf ("%d,%d" a, b);
Фактические параметры
При обращении к функции ей передаются фактические параметры, то есть выражения, имеющие те реальные значения, с которыми функция отрабатывает очередной вызов. Другими словами, при вызове функция получает данные, с которыми фактически выполняется алгоритм функции при этом обращении.
1.2.11.3. Формальные и фактические параметры функции
В описании функции ее внешние данные названы формальными параметрами. Это название подчеркивает, что данные описания формальны, абстрактны, то есть не участвуют в реальных действиях, а только описывают взаимосвязь данных в теле функции. Формальные параметры функции, это всегда имена переменных. В заголовке функции они объявляются, поэтому и должны быть указаны вместе с типом.
При обращении к функции ей передаются фактические параметры. Это выражения, значения которых известны на момент обращения к функции, и с которыми функция отрабатывает очередной вызов. Фактические параметры подставляются на место формальных при каждом обращении к функции. Они могут быть в общем случае выражениями, то есть константами, переменными, или выражениями.
Тип параметров, их количество и порядок следования называются совместно «сигнатура параметров». Есть непреложное правило Си++: в описании функции и в обращении сигнатуры параметров должны строго совпадать. Это означает, что формальные и фактические параметры должны соответствовать друг другу по количеству, типу и порядку следования.
Пример 1.
Иллюстрирует особенности, которые могут быть при обращении к функции, возвращающей значение. Показывает варианты обращения.
// Пример описания функции, которая находит среднее арифметическое трех чисел.
float Avg (float a, float b, float c)
{ float S; // Локальная переменная.
S = (a+b+c)/3.;
return S; // Тип совпадает с типом функции.
}
// Для обращения к функции Avg используется оператор-выражение.
void main(void)
{
float x1 = 2.5, x2 = 7, x3 = 3.5;
float y;
// Фактические параметры – переменные.
y = Avg (x1, x2, x3); // Обращение в присваивании.
printf ("x1=%f, x2=%f, x3=%f y= %f\n", x1, x2, x3, y);
// Фактические параметры – константы вещественного типа.
y = Avg (2., 4., 7.);
printf ("x1=%f, x2=%f, x3=%f y= %f\n" , 2., 4., 7., y);
// Фактические параметры – выражения.
y = Avg (x1*2., x2+4., sin (PI/2.));
printf ("x1=%f, x2=%f, x3=%f, y= %f\n" , 2*x1, x2+4., sin (PI/2.), y);
// Обращение в функции вывода. Фактические параметры – произвольные,
// то есть константы, переменные, выражения.
printf ("x1=%f, x2=%f, x3=%f y= %f\n" , 2., x2, x3+0.7, Avg (2., x2, x3 + 0.7);
// Как выражение, оператор-обращение может входить в другие выражения.
y = (Avg (0.5, 1.7, 2.9) + Avg (x1, x1+2, x1+2.)) * 0.5;
printf ("y= %f\n" ,y );
}
Пример 2.
При обращении к функции, не возвращающей значения, особенностей нет, это оператор-функция. Число параметров может быть произвольным. Приведем пример описания функции, которая выводит на экран 50 символов «звездочка» (*).
#include <stdio.h>
void Print_1 (void) // Функция void без параметров.
{
int i; // Локальная переменная.
for (i = 1;i <= 50;i ++)
printf("%c", '*');
printf("\n");
}
// Эту функцию легко превратить в функцию с параметром, например, которая
// выводит на экран произвольное число (count) символов «звездочка» (*).
void Print_2 (int count) // Функция void с одним параметром.
{
int i;
for (i = 1;i <= count; i ++) // Параметр участвует в управлении циклом.
printf("%c", '*');
printf("\n");
}
// Эту функцию легко превратить в функцию с двумя параметрами, например,
// которая выводит на экран произвольное число (count) произвольных символов
// (symbol).
void Print_3 (int count, char symbol) // Функция void с двумя параметрами.
{
int i;
for (i = 1;i <= count; i ++) // Параметр участвует в управлении циклом.
printf("%c", symbol); // Параметр определяет вывод.
printf("\n");
}
// Обращение к функции, не возвращающей значения, выглядит как обычный
// оператор программы. Он отрабатывает свой алгоритм и передает управление
// следующему оператору.
void main (void)
{
// Вывод заголовка.
Print_1 ();
printf ("\tПример\n");
print();
// Вывод строк различной длины. Обращение к функции в цикле.
int Сou;
for (Сou = 1; Сou <= 15; Сou ++)
Print_2 (Cou); // Cоu –текущее значение количества символов.
// Вывод строк различной длины различного содержания.
Print_3 (80, '-'); // 80 символов тире '-'.
Print_3 (25, '#'); // 25 символов решетки '#'.
}
1.2.11.4. Описание и объявление функции. Прототип функции
С точки зрения структуры программы, в Си++ все функции описываются на одном уровне. В терминах «описание» и «объявление» функции путаницы быть не должно.
Описание функции предоставляет программисту всю информацию о ней, как внешнюю (тип, имя, сигнатура параметров), так и внутреннюю (тонкости алгоритма). Если рассматривать функцию как данное конструируемого типа, то описание функции как раз и конструирует данный тип с указанным именем.
Объявление функции предоставляет только внешнюю (интерфейсную часть), для которой важно только умение правильно использовать функцию, то есть обратиться к ней. Заголовок (первая строка описания) функции как раз и предоставляет такую информацию.
Любой объект программы должен быть объявлен перед первым его использованием. Это непреложное правило относится и к функциям. Объявление или описание функции в программе должно появиться до первого обращения к ней.
Возможны варианты.
- Описание функции фактически расположено перед текстом вызывающей программы. В этом случае описание функции есть одновременно и ее объявление.
Пример.
// Две вызываемые функции без параметров. Описание опережает обращение.
void f1 (void) // Описание функции f1.
{
printf ("Функция 1\n");
}
void f2 (void) // Описание функции f2.
{
printf ("Функция 2\n");
}
void main (void) // Функции известны до момента обращения.
{
printf ("\nГлавная функция main\n");
f1 (); // Обращение к функции f1.
f2 (); // Обращение к функции f2.
}
В этом случае описание функций играет роль их объявления.
- Описание функции фактически расположено после текста вызывающей программы. Значит, обращение к функции появляется перед ее описанием. Чтобы функция стала известной вызывающей программе, кроме описания нужно еще и объявление функции. Объявление функции (в Си++ это еще называется прототип), это ее заголовок, за которым стоит знак завершения оператора «;». Прототип может быть записан в начале программы, если функция глобальна, или в теле main, если функция локализована в main, или в теле любой функции, где функция известна локально. Прототип функции, это аналог описания переменных.
Пример.
// Те же самые функции без параметров. // Прототип объявляет функции.
void f1 (void); // Прототип функции f1.
void f2 (void); // Прототип функции f2.
// Обращение опережает описание.
void main (void)
{
printf ("\nГлавная функция main\n");
f1 (); // Обращение к функции f1.
f2 (); // Обращение к функции f2.
}
// Описание функций после вызывающей программы.
void f1 (void) // Описание функции f1.
{
printf ("Функция 1\n");
}
void f2 (void) // Описание функции f2.
{
printf ("Функция 2\n");
}
- Описание функции вынесено в отдельный файл. Такие файлы должны обладать возможностью быть легко подключаемыми к любой программе, использующей эти функции. В Си++ принято создавать заголовочные файлы с именами "имя.h", в которых программист накапливает функции собственных библиотек. Этот файл будет доступен любой программе, в теле которой есть директива #include "имя.h", включающая текст заголовочного файла в код текста использующей его программы.
Пример.
// Описание функций содержится в файле с именем "Finction.h".
void f1 (void) // Описание функции f1.
{
printf ("Функция 1\n");
}
void f2 (void) // Описание функции f2.
{
printf ("Функция 2\n");
}
// В файле main.cpp обращение к функциям будет возможно, если подключен файл,
// содержащий их описание, тогда описание опережает обращение, и объявления
// функций не требуются.
#include "Finction.h" // И описания функций известны main.
void main (void)
{
printf ("\nГлавная функция main\n");
f1 (); // Обращение к функции f1.
f2 (); // Обращение к функции f2.
}
- Если файлов с исходными кодами несколько, создается проект, при этом его модули компилируются и собираются совместно. В этом случае роль заголовочного файла – сделать доступным глобальные объявления всем модулям проекта. В такие файлы выносятся глобальные объявления переменных и прототипы функций. Остальные модули содержат исходные коды функций проекта. Директива #include по-прежнему используется для «глобализации» объявлений. Исходные файлы кода с расширениями «.cpp» нужно объединить в файл проекта (опция Project), чтобы они компилировались и собирались совместно.
Пример.
Файл "Finction.h" содержит объявления функций.
void f1 (int n); // Прототип функции f1
void f2 (void); // Прототип функции f2
Описания функций f1 и f2 содержатся в файлах F1.cpp и F2.cpp.
// Файл F1.cpp
#include <stdio.h>
#include "Finction.h" // Описания функций f1 и f2.
void f1(int n)
{
printf("Функция 1.\n"); // f1 обращается к f2.
f2 ();
}
// Файл F2.cpp
#include <stdio.h>
#include "Finction.h" // Описания функций f1 и f2.
void f2 (void)
{
printf("Функция 2.\n");
}
// Файл main.cpp содержит главную функцию main, которая обращается к любой из
// функций f1 и f2. Их прототипы вынесены в файл "Finction.h".
// Файл Main.cpp
#include <stdio.h>
#include "Finction.h" // Описания функций f1 и f2.
void main(void)
{
int key;
do
{
printf ("\nГлавная функция main\n");
printf ("\nПредлагается на выбор две функции:\n");
printf ("\n1. Выбор первой.\n");
printf ("\n2. Выбор второй.\n");
printf ("\n3. Выход.\n");
scanf("%d",&key);
switch (key)
{
case 1 :
{ f1 (2); printf("Press any key…"); getch(); break; }
case 2 :
{ f2 (); printf("Press any key…"); getch(); break; }
default :
{ printf("Exit…"); getch(); exit(1); }
}
}
while (key != 3);
}
1.2.11.5. Передача параметров в функцию. Изменяемые значения параметров
В Си++ существуют два способа передачи параметров в функцию.
- По значению. До сих пор рассматривался синтаксис именно этого способа передачи данных. Его механизм, это создание локальной копии параметра в теле функции. В таком случае значения параметров не могут быть изменены в результате обращения. Это возможность защитить входные данные от их нежелательного изменения функцией.
- По ссылке. Изменение механизма передачи данного в функцию формально выглядит добавлением признака адресной операции к имени параметра в описании функции:
тип_ функции имя_функции (тип_параметра & имя_параметра)
Механизм такого способа передачи данных заключается в том, что функция и вызывающая программа работают с адресом объекта в памяти (фактически, с одной и той же областью данных). Как результат, значения параметров функция может изменить. Как следствие, фактическим параметром, соответствующим формальному параметру – ссылке, может быть только имя переменной.
Пример.
Функция находит площадь и периметр треугольника, заданного длинами сторон. Возвращаемых значений два – площадь и периметр. Эти значения функция вернет через параметры. Кроме того, нужна проверка условия существования треугольника. Это логическое значение функция вернет через return (1, если треугольник существует, и 0, если нет).
#include <stdio.h>
#include <math.h>
int Triangle (float a, float b, float c, float & p, float &s)
{
// Эта функция имеет два варианта выхода.
// Параметры a,b,c передаются по значению (только входные данные),
// параметры p, s, по ссылке (как входные данные, так и результат).
float pp; // Полупериметр.
if (a+b <=c || a+c<=b || b+c<=a) // Треугольник не существует.
return 0;
else
{ // Треугольник существует.
p = a + b + c;
pp = 0.5 * p;
s = sqrt (pp* (pp – a)*(pp – b)*(pp – c));
return 1;
}
}
// При обращении фактическими параметрами, подставляемыми на место
// формальных параметров-значений, могут быть выражения, а фактическими
// параметрами, подставляемыми на место формальных параметров-адресов, могут
// быть только переменные.
void main (void)
{
float A, B, C; // Длины сторон фактические.
float Perim, Square; // Периметр и площадь фактические.
// Пример обращения 1.
printf("Введите длины сторон треугольника\n");
scanf ("%f%f%f", &A, &B, &C);
if (Triangle (A, B, C, Perim, Square) = = 1)
printf("Периметр равен %6.2f, площадь равна %6.2f\n", Perim, Square);
else
printf ("Треугольник не существует\n");
}
1.2.11.6. Механизм обращения к функции и передачи данных
При обращении к функции происходит следующая цепочка событий (рекомендуется наблюдать данный процесс в отладчике):
- Управление передается в функцию.
- Выделяется память для параметров функции, вычисляются значения формальных параметров и копируются в локальную память. (Так передаются внешние данные, необходимые для работы функции).
- Создаются локальные переменные функции (те, которые объявлены в теле функции).
- Выполняется алгоритм функции.
- По оператору return управление передается в точку вызова, при этом в вызывающую программу передаются новые данные (результат функции).
- Локальные переменные, в том числе формальные параметры, умирают, память высвобождается.
1.2.11.7. Локальные и глобальные данные. Время жизни и область действия переменных.
Программа на языке С++ (модуль) может состоять из одного или нескольких текстовых файлов с расширением ".с" или ".cpp". Если файлов несколько, то они включаются в файл проекта с расширением ".prj" (опция интегрированной среды Project). При этом исходные файлы проекта компилируются и собираются совместно. Проект может состоять из одной или нескольких функций. Функция main должна быть одна, с нее начинается выполнение проекта. В каждом файле может быть одна или несколько функций, их разделение выполняется на логическом уровне. Программные единицы проекта взаимодействуют друг с другом по принципу разделения управления и передачи данных.
Все функции внешние, т.е. их описания выполняются на одном уровне. Любая функция, кроме "main", вызывается из другой функции. При вызове функции ей могут быть переданы параметры, по окончании выполнения функции, в вызывающую функцию может быть возвращено значение.
Объявление функции (с использованием прототипа) может быть выполнено на любом уровне. Если на внешнем уровне, то функция доступна всему файлу, если в теле функции, то доступна только этой функции.
Дадим несколько определений.
Определение. Объект, это сущность, обладающая некоторыми атрибутами (свойствами) и методами для проверки и изменения атрибутов объекта.
В тексте программы под объектами понимают переменные, именованные константы, функции, в целом, имена данных различных типов. Объект присутствует в программе своим именем. Каждый объект должен быть объявлен.
Определение. Область действия объекта, это область программного кода, в которой объект известен (то есть действует его объявление). Если объект объявлен в начале программы, вне тела всех функций, то он известен везде внутри того файла, где объявлен.
Определение. Время жизни объекта, это понятие, связанное с областью действия, период времени в процессе выполнения программы, когда объект фактически занимает память (память выделяется при объявлении).
Определение. Локальные (внутренние) объекты объявлены внутри тела блока. Локальные объекты функции объявлены внутри тела функции.
Область действия локального объекта – блок, в котором он описан. Описание действует от точки описания до конца блока.
Время жизни локального объекта – только время выполнения блока. При входе в блок память выделяется, при выходе память освобождается.
Определение. Глобальные (внешние) объекты объявлены вне тела функции на внешнем уровне.
Область действия глобального объекта – от точки объявления до конца файла с кодом программы, в котором объявлен объект.
Время жизни глобального объекта – время выполнения программы.
Весьма приблизительную иллюстрацию может дать рисунок 1.2.11.7.1.
Рис 1.2.11.7.1. Область действия локальных и глобальных объектов
Параметры функций по механизму действия тоже можно отнести к локальным или глобальным, а именно:
- Параметр, передаваемый по значению, это локальный объект (копия данного в блоке). Это мощное средство защиты данных от нежелательного их изменения функцией.
- Параметр, передаваемый по ссылке, действует по механизму глобального объекта. Это единый объект, так как функция и вызывающая программа работают с одним адресом объекта. Это мощное средство для того, чтобы сделать открытым адресное пространство.
1.2.11.8. Принцип сокрытия данных внутри функции. Принцип локализации имен
Этот термин имеет много синонимов, например, пространство имен, приоритет имен, принцип сокрытия имен. Применяется в случае, когда локальное имя какого-нибудь объекта в теле функции совпадает с именем глобального объекта. Что касается выбора имен, то имя любого объекта в любом блоке уникально, но в разных блоках имена могут повторяться, неся различную смысловую нагрузку. Особенно это касается имен рабочих переменных.
Пример.
int A = 90;
…
void F(int x)
{
int A = 0; // Каким же будет значение А в теле функции?
…
}
Принцип локализации имен заключается в том, что на время действия локальной переменной глобальная переменная с тем же именем временно прекращает свое существование. Она возобновляет свое значение, как только заканчивается время жизни локальной переменной (при завершении работы функции).
Пример. Чтобы увидеть механизмы локализации данных и принцип локализации имен, этот пример следует выполнять в отладчике.
#include <stdio.h>
#include <conio.h>
// Глобальные данные программы.
int a;
int n = 1;
void f1 (void); // Объявление функций глобально.
void f2 (void);
void f3 (int);
void main(void)
{
int i; // Локальная переменная функции main
a = 10; // Функция main изменяет глобальную переменную.
// Все функции имеют одинаковые права на изменение глобального данного.
for (i = 1; i <= 3; i++)
{
n ++; // main изменяет значение n, нумеруя последовательность
// вызовов функции f1.
printf ("\nMain. Вход № %d", n);
f1 (); // f1 тоже меняет значение n, потому что
} // main и f1имеют одинаковые права на изменение n.
for (i = 1; i <= 3; i++)
{
a ++; // main изменяет значение a.
printf ("\nMain. Глобальная а: %d", a);
f2 (); // В функции f2 значение а свое собственное.
}
// Как передача данного в функцию защищает его от изменения.
f3 (a);
printf ("\nЗначение глобального а после обращения к f3 %d", a);
}
// f1 печатает номер обращения.
void f1 ()
{
n ++; // Функция меняет глобальную переменную n.
printf ("\nФункция f1. Вход %d", n);
}.
// f2 печатает значение локальной переменной.
void f2 ()
{
int a = 90; // а – локальное данное функции f2.
printf "\nФункция f2. Локальная а %d", a);
}
//f2печатает значение локальной переменной.
void f3 (int a) // Глобальная переменная передается
{ // как параметр, защищена от изменения.
a ++; // Локальное данное функции f3.
printf("\nФункция f3. Не меняет значение глобального а %d", a);
}
Выводы.
Время жизни и область действия переменных определены их объявлением.
Механизм взаимодействия программных единиц друг с другом, это разделение передачи управления и передачи данных.
Принцип сокрытия данных внутри функции защищает их от нежелательного изменения.
1.2.11.9. Рекурсивные функции
Определение. Рекурсивной называется функция, которая сама вызывает себя. Вызов может быть прямым или косвенным, соответственно, различают прямую или косвенную рекурсию.
Прямая рекурсия имеет место, если в теле функции явно используется вызов этой же функции. Функция является косвенно рекурсивной, если она содержит обращение к другой функции, содержащей прямой или косвенный вызов первой функции. В этом случае по тексту описания функции ее рекурсивность может быть не видна.
Механизм реализации рекурсии, это отложенные обращения к функции. При каждом обращении к рекурсивной функции создается копия значений ее параметров, записываемых в стек, после чего управление передается первому исполнимому оператору функции. Вызовы откладываются до тех пор, пока в теле функции не будет выполнено условие прекращения обращений, после чего все вызовы обрабатываются в обратном порядке.
Рассмотрим синтаксис функции, вычисляющей сумму чисел натурального ряда. Этот алгоритм является, в некоторой степени, рекурсивным, так как каждое новое значение суммы S = S + i вычисляется «на основании» предыдущего значения. Такие соотношения называются реккурентными. Функция Sum_Iter (int n) показывает вычисление суммы с помощью обычного итерационного цикла, использующего в теле реккурентное соотношение. Функция Sum_Rec (int n) описана как рекурсивная, и показывает вычисление суммы с помощью рекурсии.
#include <stdio.h>
int Sum_Iter (int n)
{
for (int i = 1,Sum = 0; i <= n; i++)
Sum += i; // Реккурентное соотношение
return Sum;
}
int Sum_Rec (int n)
{
if(n = = 1)
return 1;
else
return n + Sum_Rec (n–1); // Рекурсивный вызов.
}
// Обращение выполняется обычным образом.
void main(void)
{
printf ("Итерационная сумма = %d\n", Sum_Iter(5));
printf ("Рекурсивная сумма = %d\n", Sum_Rec(5));
}
Опишем механизм обращения к этой функции. Для любого положительного аргумента функция возвращает результат, равный значению суммы, вычисленной на предыдущем шаге (параметр на 1 меньше), плюс очередное слагаемое: Sum_Rec (n) = Sum_Rec (n–1) + n, затем Sum_Rec (n–1) = Sum_Rec (n–2) + (n–1), и так далее. Последовательность рекурсивных обращений к функции прерывается при вызове Sum_Rec (1). Этот вызов приводит к последнему значению суммы, равному 1, так как последнее выражение, из которого вызывается функция, имеет вид Sum_Rec (n–2) + 1. Затем отложенные вызовы обрабатываются в обратном порядке, и к уже известному значению добавляются новые, вычисленные на предыдущем шаге. Этот процесс хорошо виден в отладчике.
Использование рекурсивного алгоритма в этом примере не является наилучшим, и приведено только для того, чтобы показать принципиальные особенности синтаксиса рекурсивной функции. Достоинство рекурсии – простота и логичность записи, недостаток – расход времени и памяти на обработку вызовов. Имеет смысл использовать такие алгоритмы в тех задачах, где рекурсия использована в определении обрабатываемых данных, например, для динамических структур данных с рекурсивной структурой (очереди, деревья).
1.2.11.10. Проектирование функции
1) Выделение самостоятельного алгоритма, его входных, выходных и локальных данных. Определение типов.
2) Описание функции.
1.2.11.11. Классы памяти. Внешние объекты
Существуют классы памяти, к которым отнесена переменная. Есть три класса памяти: auto, static, register. Они классифицируют переменные по схеме выделения памяти. Приписываются перед объявлением типа переменной, например,
auto int i;
static int n;
register int k;
Класс auto называется автоматическим классом памяти, действует по умолчанию. Регистровый класс памяти (register) раньше использовался для того, чтобы явно указать компилятору, что этот объект должен непосредственно размешаться в регистрах процессора, что позволяло повысить быстродействие программ. Компиляторы современного уровня являются оптимизирующими, и сами определяют механизмы выделения памяти, поэтому объявление register носит рекомендательный характер, и объявленный объект будет, скорее всего, класса auto. Класс static называется статическим классом памяти, используется для того, чтобы вынести на глобальный уровень объявление локального объекта.
Механизм действия классов памяти зависит от того, как локализован объект.
Для локальных объектов:
Объекты auto существуют только внутри того блока, где они определены. Память для объекта выделяется при входе в блок, а при выходе освобождается, то есть объекты перестают существовать. При повторном входе в блок для тех же объектов снова выделяется память, значения переменных не сохраняются.
Другими словами, объекты класса auto локализованы в блоке, где определены, а время их жизни равно времени выполнения блока, следовательно, автоматическая память всегда внутренняя.
Объекты static существуют в течение всего времени выполнения программы. Память выделяется один раз при старте программы и при этом обнуляется. При повторном входе в блок, где объявлен статический объект, его значение сохраняется. Вне блока, где объявлен статический объект, его значения недоступны.
Другими словами, объекты класса static локализованы в блоке, где определены, но время их жизни равно времени выполнения всей программы. Статическая память внутренняя по отношению к блоку.
Пример.
void f_auto (void)
{// Переменная K по умолчанию auto.
int K = 1; // Локальный объект автоматической памяти.
printf ("K = %3d ", K);
K++;
return;
}
void main (void)
{
for (int i = 1; i <=3; i ++)
f_auto ();
}
Результат выполнения программы обусловлен локализацией объекта К:
К = 1 К = 1 К = 1
В том же примере покажем действие статического объекта.
void f_stat (void)
{// Переменная K статическая.
static int K = 1; // Локальный объект статической памяти.
printf ("K = %3d ", K);
K++;
return;
}
void main (void)
{
for (int i = 1; i <=3; i ++)
f_stat ();
}
Результат выполнения программы обусловлен «глобализацией» объекта К изменением класса памяти:
К = 1 К = 2 К = 3
Вне тела функции f_stat объект недоступен, то есть он внутренний.
Для глобальных объектов:
Объекты auto действуют в данном файле от описания (auto) до конца файла. Известны всем функциям программы. Не действуют в других файлах проекта, где они описаны с атрибутом extern.
Объекты static действуют от описания до конца файла, известны всем функциям программы. Не действуют в других файлах проекта, где они описаны с атрибутом extern.
Внешние объекты. Этот термин используется для именования объектов, в описании которых присутствует ключевое слово extern. Как правило, программа на Си++, это совокупность отдельных модулей (файлов), каждый из которых имеет в своем составе некоторый набор функций обработки данных. Все функции в Си++ являются внешними, так как объявлены на одном уровне. В Си++ можно объявить и внешние объекты – переменные любого типа. Ранее мы называли их глобальными, имея в виду, что их определение действует в рамках одного модуля (файла). Они объявляются вне тела всех функций модуля, и доступны всем функциям этого модуля. Когда несколько модулей объединяются в проект, внешние объекты каждого модуля могут быть доступны многим функциям программы, в том числе функциям других модулей, но не всегда эта доступность достигается автоматически. Для того, чтобы сделать объект доступным для функций другого файла или функций, объявленных выше по тексту, используется ключевое слово extern.
Если объект объявлен с ключевым словом extern в начале файла, то он доступен всем функциям этого файла. Если такое объявление в теле функции, то объект локален в теле функции. Чтобы сделать объект доступным в нескольких модулях, его следует определить в одном модуле как глобальный, а в других модулях проекта как внешний с помощью extern.
Во внешнем объявлении прототипов всех функций по умолчанию класс памяти принят extern.
1.2.11.12. Организация памяти на машинах класса PC. Модели памяти
Память организована по страничному принципу. В стандартном режиме каждая страница состоит из памяти объемом 64 Кб. Адрес страницы задается в виде 16-разрядного слова (адрес сегмента). Адрес на странице задается тоже в виде 16-разрядного слова, считая с начала страницы (смещение). Общий адрес получается путем сложения этих адресов со сдвигом адреса страницы влево на 4 разряда. Адрес сегмента располагается в регистре DS, а адрес на странице в регистре общего назначения CS.
Таким образом, общий адрес объекта может состоять из 20-ти разрядов, что обеспечивает адресацию в пределах 1 Мбт. Кроме того, разные пары «сегмент-смещение» могут указывать на один и тот же адрес объекта, а разные страницы могут быть организованы с перекрытием друг друга. Поэтому адреса объектов могут состоять только из адреса на странице, если все данные размещаются на в 64 Кб (вид адресации near – ближний), или из адреса вида «сегмент-смещение» (вид адресации far – дальний). Поскольку разные пары «сегмент-смещение» дают один и тот же адрес, то сравнение этих указателей даст неверный результат. Например, 0:40 и 2:8. Для устранения этого недостатка с Си++ введен еще один вид 32-х разрядного адреса (вид адресации huge), который нормирован относительно адреса сегмента. Он наибольший из возможных, то есть, выровнен относительно параграфа 4 бита, в этом случае сравнение указателей всегда дает правильный результат.
В этой связи реальное объявление указателя может иметь вид:
Модификатор Тип_указателя Вид * Имя_объекта
Вид указателя задается одним из ключевых слов near, far или huge. Если вид указателя опущен, по умолчанию он определяется видом модели памяти.
Существует шесть моделей памяти, которые определяют ограничения на количество данных в программе, на типы указателей, объем памяти, занимаемый программой, и так далее. Модели памяти можно задать средствами оболочки Си++ в опциях компилятора.
Крохотная (tiny). Все четыре сегментных регистра (CS, DS, SS, ES) указывают на один и тот же адрес. Код программы и данные находятся в одном сегменте и занимают не более 64 Кб. В программе по умолчанию используются только near указатели.
Малая (small). Программный сегмент и сегмент данных различны, и не перекрываются. Итого программа может иметь размер 64+64=128 Кб памяти. Сегменты стека и дополнительные сегменты начинаются с того же адреса памяти, что и сегмент данных. В программе по умолчанию используются только near указатели.
Средняя (medium). Для программы используются far указатели, для данных near. Итого код программы может быть до 1 Мб, а объем статических данных, до 64 Кб.
Компактная (compact).Для программы используются near указатели, для данных far. Итого код программы может быть до 64 Кб, а объем данных (не статических) до 1 Мб. Эта модель противоположна средней модели.
Большая (large). Для программы и данных используются far указатели. Объем статических данных ограничен до 64 Кб. Используется для создания больших программных продуктов.
Огромная (huge).Для программы и данных используются far указатели. Все ограничения на объем сняты.
1.3. Продвинутые возможности языка Си++ и производные типы данных
1.3.1. Классификация типов данных С++
Классифицируя данные Си++, мы отметили, что есть две группы типов: базовые типы и производные типы.
Синонимами названия «базовый тип» являются названия «стандартные», «предопределенные». Реализация этих типов заложена в стандарте языка, и программист не вправе изменить предопределенное. Производным типом является такой тип, который должен быть описан пользователем перед употреблением. Полезно напомнить, что тип данного определяет его размещение в памяти и набор операций.
Примерная классификация производных типов.
- Функции.
- Массивы.
- Указатели.
- Структуры.
- Объединения.
- Файлы.
1.3.2. Массивы
Определение. Массив – упорядоченное множество данных одного типа, объединенных общим именем.
Тип элементов массива может быть почти любым типом С++, не обязательно это базовый тип. Например, можно определить массив координат точек на плоскости, тогда один элемент массива содержит две координаты одной точки. Можно определить массив сведений о студенте, тогда один элемент массива содержит разнообразные сведения о студенте, например, фамилия, имя, возраст, адрес и прочие. В этих случаях для определения типа одного элемента массива используется также производный тип, например, массив или структура.
Обязательные атрибуты массива, это размерность, число элементов и тип элементов. В стандарте С++ определены только одномерные массивы.
1.3.2.1. Описание массива
Чтобы ввести в употребление массив, необходимо его описать (конструировать). Можно одновременно выполнить инициализацию элементов массива. Смысл описания и объявления для массивов идентичен.
Описание массива выполняется, как и для обычных переменных, в разделе описаний. Синтаксис:
тип_массива имя_массива [количество_элементов];
Здесь количество_элементов, это константа, заданная явно или именованная (константное выражение).
Пример.
#define N 10
int mas [3]; // Одномерный массив mas содержит 3 целочисленных элемента.
int matr [2][10];// Одномерный массив matr содержит два одномерных
// массива по N = 10 элемента в каждом.
float w [N]; // Одномерный массив w содержит N вещественных элементов.
char c [5][80]; // Массив из 5-ти строк, по 80 символов в строке.
Примечание. Элементы массива нумеруются с 0, т.е. для
int mas [3];
имеются элементы mas [0], mas [1], mas [2].
1.3.2.2. Размещение в памяти элементов массива.
При объявлении массива имя массива сопоставлено всей совокупности значений. Элементы массива размещаются в памяти подряд в соответствии с ростом индекса (номера элемента внутри массива). Размер выделяемой памяти такой, чтобы разместить значения всех элементов. Такие массивы называются статическими, место в памяти выделяется на этапе компиляции, именно поэтому размер массива определен константой.
Чтобы определить общий размер памяти, занимаемой массивом, можно использовать операцию sizeof. Как известно, ее аргументом может быть имя объекта или имя типа, в том числе объекта сконструированного типа.
sizeof (имя_переменной) // В байтах для этой переменной.
sizeof (имя_типа) // В байтах для любого данного этого типа.
sizeof (имя_массива) // В байтах для этой переменной с учетом длины.
1.3.2.3. Операции над массивами
Над массивом как единой структурой никакие операции не определены. Над данными, входящими в массив, набор операций определен их типом. При работе с массивом можно обращаться только к отдельным его элементам. Операция обращения к одному элементу массива называется операцией разыменования. Это бинарная операция [ ], синтаксис которой:
Имя_массива [индекс]
Первый операнд Имя_массива показывает, что происходит обращение к необычному данному, то есть такому, в составе которого много значений, то есть ко всем элементам массива.
Второй операнд индекс может быть только целочисленным, дает возможность выделить один элемент из группы, указывая его номер внутри массива. Индекс, в общем случае, выражение целого типа, определяющее номер элемента внутри массива (счет с нуля), например:
mas [0]; // Обращение к первому элементу массива (номер его =0).
matr [1][3]; // Обращение к элементу матрицы, стоящему на пересечении
// 1-ой строки и 3-го столбца.
char c[i][2]; // Обращение ко 2-му символу i - той строки текста.
Замечание. Контроль выхода индекса за границы массива не существует.
Покажем размещение элементов массива в памяти на рис. 1.2.2.3.1.
int mas [4];
|
Имя mas сопоставлено всей совокупности данных
|
|
…
|
mas[0]
|
mas[1]
|
mas[2]
|
mas[3]
|
…
|
|
Индекс=0
|
Индекс=1
|
Индекс=2
|
Индекс=3
|
|
Рис. 1.2.2.3.1. Размещение в памяти элементов массива
Для каждого элемента массива выделено по 2 байта, как для данного типа int. В целом для всего массива выделено 2 * 4 = 8 байт. Значения элементов массива неизвестны.
1.3.2.4. Начальная инициализация элементов массива
При описании массива можно выполнить начальную инициализацию значений его элементов, для этого нужно задать список инициализирующих значений. В списке инициализации перечисляются через запятую значения элементов массива. Например, в году всегда 12 месяцев, значение числа дней в каждом месяце известно, значит, такая структура может быть задана массивом:
int month [12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
Эквивалентом инициализации является простое присваивание вида:
month [0] = 31; // Январь
.. и т.д.
month [11] = 31; // Декабрь
Механизм инициализации прост, но имеет некоторые особенности:
1. Если значение размера массива опущено, то число элементов массива равно числу значений в списке инициализации:
int month [] = {31, 28, 31}; // Количество элементов равно 3.
Чтобы программно определить число элементов в таком массиве, используется операция sizeof:
int len; // Число элементов.
…
len = sizeof (month ) / sizeof (int);
2. Если число элементов в списке инициализации меньше, чем объявлено в описании, то число элементов массива будет, сколько объявлено, а остальные значения будут равны 0.
int month [12] = {31, 28, 31, 30}; // Количество элементов равно 12.
// Их значения 31, 28, 31,30,0,0,0…
3. Если число элементов в списке инициализации больше, чем объявлено в описании, то это синтаксическая ошибка.
int month [2] = {31, 28, 31, 30}; // Ошибка.
1.3.2.5. Массивы переменной длины.
Одним из недостатков статических массивов, обусловленным требованиями реализации, является то, что длина такого массива в тексте программы определена константным выражением, следовательно, жестко задана в программе. Часто бывает необходимо, чтобы число элементов массива изменялось бы при работе программы. В полной мере такие массивы реализуются с использованием динамической памяти, механизмы работы с которой изложены далее. Существуют приемы, которые используются, чтобы работать с массивом, длина которого может изменяться.
1. #define определенные константы.
2. Массив условно переменной длины.
3. Динамические массивы.
Рассмотрим два первых приема. Механизм #define определенных констант заключается в изменении текста программы перед ее компиляцией, значит, препроцессор может редактировать текст программы, внеся в него любое количество изменений. Длина массива записывается в директиве #define, например:
#define N 20
Имя N теперь именованная константа, и в тексте программы для управления алгоритмами обработки массива следует использовать ее имя. Числовое значение константы 20 записывается в тексте один раз в самом начале, и перед очередным запуском программы может быть изменено один раз, остальные изменения выполнит препроцессор. После этого программа нуждается в повторной компиляции и сборке.
Пример. Выполним ввод данных массива и вывод на печать.
#define N 10 // Статический массив Длина массива равна 10.
// В циклах управления вводом и выводом используется не значение 10, а имя N.
// Числовое значение константы записано тексте один раз, и перед повторной
// компиляцией достаточно внести в текст программы всего одно изменение.
void main (void)
{
int a[N];
int i;
printf("\nВведите %d значений\n", N); //
for (i = 0; i < N; i++)
scanf("%d", &a[i]);
for (i = 0; i < N; i++)
printf("%5d", a[i]);
printf ("\n");
}
Смысл второго приема заключается в том, что длина массива, даже если она изменяется, может быть оценена заранее. Если знать, какова возможная наибольшая длина массива, то именно это значение и следует выбрать для описания массива. Для того чтобы знать реальную длину массива, вводится специальная переменная, которая принимает значение при выполнении программы, а затем использует его для управления алгоритмами обработки массива. Оба приема можно совместить.
Пусть задан массив, в котором есть как положительные, так и отрицательные числа. Требуется разделить этот массив на два новых массива, в первый из которых записать только положительные числа, а во второй только отрицательные. Длина исходного массива не более 50-ти. В этой задаче длина каждого из новых массивов неизвестна заранее, и может быть получена только в процессе работы программы, но очевидно, что даже в предельном случае она не превысит длину исходного массива. Поэтому в описаниях массива их длины следует определять по максимальному значению. Для обозначения реальных длин массивов введены переменные, которые должны принять значение в процессе выполнения программы.
В этом же примере покажем, как можно присвоить значения элементам массива с помощью функции генерации случайных чисел random(). Эта функция находится в библиотеке <stdlib.h>, и имеет синтаксис:
int random (int num).
Она возвращает целое значение в диапазоне от 0 до num – 1. Чтобы запустить генератор случайных чисел, в начале программы следует записать функцию randomize () библиотеки <time.h>. Ее достаточно выполнить один раз, в то время как random должна быть вызвана для получения каждого элемента массива.
#define N 50 // Наибольшая длина массива.
#include <stdlib.h>
#include <time.h> // Для функций randomize и random
void main(void)
{
int a[N], b[N], c[N]; // Три массива, наибольшая длина каждого N.
int i;
int len_a, len_b, len_c; // len_a, len_b, len_c – реальные длины каждого.
randomize(); // Записывается один раз в начале.
printf ("Введите количество элементов в исходном массиве.\n");
scanf ("%d", &len_a);
// Получение значений элементов исходного массива. random генерирует значения
// в диапазоне от 0 до 49, вычитаем 25, чтобы получились отрицательные.
for (i = 0; i < len_a; a [i++] = random(50)-25); // Тело цикла в заголовке.
printf("Исходный массив:\n");
for (i = 0; i < len_a; i++)
printf("%4d",a[i]);
printf("\n");
// Два пустых массива. Их длины предварительно принимают значение 0.
len_b = 0;
len_c = 0;
// Исходный массив сканируется, положительные числа записываются в массив а,
// отрицательные в b. Переменные len_b, len_c в цикле управления играют роль
// рабочих переменных, накапливая значение по мере записи элементов в массив.
for (i = 0;i < len_a; i++)
if (a[i] > 0)
b[len_b++]=a[i];
else
c[len_c++]=a[i];
printf("Новый массив:\n");
for (i = 0; i < len_b; i++) // Число элементов len_b–1.
printf ("%4d",b[i]);
printf ("\n");
printf ("Новый массив:\n");
for (i = 0; i < len_c; i++) // Число элементов len_с–1.
printf("%4d",c[i]);
printf("\n");
}
1.3.2.6. Алгоритмы работы с одномерными массивами
Алгоритмы работы с одномерными массивами основаны на использовании циклических алгоритмов, в которых выполняется последовательное обращение к элементам массива. Управляющей переменной в таких алгоритмах должен быть индекс массива.
Приведем фрагменты алгоритмов решения некоторых задач для массивов. Будем ориентироваться на абстрактное решение задачи для произвольного массива произвольной длины. Такую возможность дает только использование функций обработки массивов. В функцию следует передать массив (передается указатель) и длину массива. Если функция не изменяет длину массива, она передается по значению, если изменяет, то по адресу. Объявление фактических массивов, то есть тех, с которыми фактически будет работать функция, и присваивание значений их элементам происходит в вызывающей программе.
1. Прямой поиск.
Поиск, это одна из наиболее часто решаемых задач, заключается в том, что в массиве требуется найти одно или несколько значений, удовлетворяющих какому-то, заранее заданному, условию. Механизм поиска предельно прост. Нужно сканировать последовательно (прямо) все элементы массива, проверяя каждый на соответствие заданному условию. Иногда это называется «прямой перебор».
Пусть условие будет «значение элемента массива четное». С каждым найденным элементом можно выполнить какое-то действие, например, вывести на экран.
int Found_chot (int a[], int n)
{
for (int i = 0; i < n; i++)
if (a[i] % 2 = =0)
printf ("Элемент найден, его номер \
= %d, значение % d = \n", i, a[i]);
return 1;
}
2. Использование флага.
Приведенный алгоритм имеет существенный недостаток. Если в массиве не найдется ни одного значения, удовлетворяющего условию, то функция вывода printf не будет выполнена ни разу, и экран останется чистым. Алгоритм будет лучше, если при неудачном поиске на экран вывести сообщение об этом. Чтобы при поиске запомнить хотя бы один факт удачного сравнения, вводится специальная переменная, называемая «флаг», которая принимает логическое значение, определенное смыслом алгоритма, например, 1, если поиск удачен, и 0, если поиск неудачен.
int Found_chot (int a[], int n)
{
int flag;
flag = 0; // Полагаем, что поиск будет неудачен,
// флаг опущен.
for (int i = 0; i < n; i++)
if (a[i] % 2 = = 0)
{
flag = 1; // Поиск удачен, флаг поднят.
printf ("Элемент найден, его номер \
= %d, значение % d = \n", i, a[i]);
}
// Проверка состояния флага.
if (flag = = 0)
printf ("Четных элементов нет\n");
return flag;
}
Обратите внимание, что функция возвращает значение флага, которое может быть использовано в вызывающей функции, например:
if (flag (mas,10))
// Одно решение
else
// Другое решение
3. Поиск первого (последнего) вхождения элемента в массив.
Суть алгоритма в том, что выполняется прямой поиск, который необходимо остановить на первом вхождении элемента, удовлетворяющего условию. Для поиска последнего элемента цикл перебора следует организовать с конца массива. Следует предусмотреть, что искомого элемента может не оказаться. Найдем элемент, который по модулю больше некоторого наперед заданного значения М. Функция вернет номер (индекс) найденного значения или 0, если такого нет.
int Found_first (int a[], int n, int M)
{
int i = 0;
// Из цикла перебора есть два способа выхода:
// 1) когда abs (a[i]) > M, цикл прерывается на текущем значении i;
// 2) когда достигнут конец массива, i >= n.
while (abs (a[i]) <= M && i < n)
i++;
if (i > n)
return 0;
else
return i;
}
В вызывающей функции следует анализировать возвращаемое значение, например:
int Ind = Found_first (mas, len, 25);
if ( Ind != 0)
printf ("Номер первого элемента > 25 = %d, значение %d\n", Ind, mas[Ind]);
else
printf ("Элементов, > 25, нет в массиве\n");
…
Другой вариант этого же алгоритма, но с использованием цикла управления for, покажем на примере поиска последнего вхождения элемента.
int Found_last (int a[], int n, int M)
{
// Из цикла перебора есть два способа выхода:
// 1) как только найдено abs (a[i]) > M, цикл прерывается на текущем
// значении i, и происходит выход из функции по return i;
// 2) когда достигнут конец массива, цикл заканчивается естественным
// образом, и если в его теле не было прерывания,
// то происходит выход из функции по return 0.
for (int i = n – 1; i >=0; i– –)
if (abs (a[i]) > M)
return i;
return 0;
}
4. Алгоритмы с изменением длины массива.
К таким алгоритмам относятся удаление элемента из массива и вставка элемента в массив. Обычно они не решаются как самостоятельные задачи, а входят в алгоритм поиска. Например, удалить из массива все элементы, удовлетворяющие какому-то условию, или добавить элемент в упорядоченный массив с сохранением упорядоченности. В любом случае в массиве первоначально должно быть определено место, в котором выполняется операция (точка вставки или удаления). Это индекс элемента, который должен быть удален, или индекс элемента, за которым должен появиться новый элемент. Обе операции приводят к изменению длины массива.
Рассмотрим эти задачи как самостоятельные в предположении, что точка вставки (удаления) известна. Обозначим ее индексом k.
При удалении элемента происходит сдвиг части массива влево на одну позицию, начиная с k-того элемента: a[k] = a[k+1], a[k+1] = a[k+2] и так далее до конца массива, a[n–2] = a[n–1]. Каждый элемент записывается на место предыдущего, стирая его. В итоге длина массива уменьшается на 1. Суть происходящих событий показана на рис. 1.2.2.6.1.
|
12
|
|
22
|
34
|
55
|
|
45
|
99
|
|
|
a[0]
|
…
|
a[k]
|
a[k+1]
|
a[k+2]
|
…
|
a[n–2]
|
a[n–1]
|
|
Рис. 1.2.2.6.1. Алгоритм удаления элемента из массива.
Приведем фрагмент программы, реализующей этот алгоритм.
int j;
int k = 4; // Номер удаляемого элемента = 4.
…
for (j = k; j < n – 1; j++)
a [j] = a [j+1]; // Цикл сдвига закончился.
n – –; // Длина массива стала меньше.
…
При добавлении элемента в массив сначала нужно освободить место для записи нового значения. Для этого нужно выполнить сдвиг части массива вправо на одну позицию, начиная с последнего элемента: a[n] = a[n–1], a[n–1] = a[n–2] и так далее до точки вставки a[k+1] = a[k]. Последний элемент записывается на свободное место за пределами массива, далее каждый элемент записывается на освободившееся от последующего элемента место, стирая его. После такой подготовки выполняется запись в точку вставки нового значения. В итоге длина массива увеличивается на 1. Суть происходящих событий показана на рис. 1.2.2.6.2.
|
12
|
|
22
|
34
|
55
|
|
45
|
99
|
|
|
a[0]
|
…
|
a[k]
|
a[k+1]
|
…
|
a[n–2]
|
a[n–1]
|
a[n]
|
|
Рис. 1.2.2.6.2. Алгоритм добавления элемента в массив.
Приведем фрагмент программы, реализующей этот алгоритм.
int j;
int k = 2; // Новый элемент добавим на место второго элемента.
int new_el = 99; // Новый элемент new_el.
…
for (j = n; j >= k–1; j– –)
a[j] =a[j–1];
// После того, как место подготовлено, запишем новый элемент.
a [k]=new_el;
n++; // Длина массива стала больше.
…
5. Использование сложного цикла для работы с массивом.
Использование сложных, точнее, вложенных циклов при работе с массивами необходимо всегда при более сложной обработке, чем простой поиск, просмотр и им подобные. Если в результате поиска над массивом происходят какие-то изменения, то это, чаще всего, вложенный цикл. Приведем пример реализации алгоритма сортировки массива, например, по возрастанию. Используем одну из вариаций пузырьковой сортировки, суть которой заключается в следующем: для каждого элемента массива (назовем его опорным) выполняется поиск другого элемента, который меньше его по значению. Поиск выполняется только в оставшейся части массива (правее того, для которого выполняется поиск). Когда найден элемент, меньший, чем опорный, их значения меняются местами. Таким образом, элемент с меньшим значением перемещается влево. Как видим, в одном алгоритме требуется объединить два: сохранять положение опорного элемента и выполнять для него поиск в оставшейся части массива. Примерное изложение событий показано на рис. 1.2.2.6.3.
Рис. 1.2.2.6.3. Алгоритм сортировки массива
Приведем текст программы, реализующей этот алгоритм.
#define N 10
void main(void)
{
int A[N]={10, 9, 8, 7, 6, 5, 4, 3 ,2, 1};
int buf;
int i, j;
for (i = 0; i < N – 1; i ++) // Цикл работы с опорным элементом.
for (j = i+1; j < N; j++) // Цикл поиска и замены.
{
if (A [i] > A [j])
{
buf = A [i] ;
A [i] = A [j];
A [j] = buf;
}
}
printf ("\n По возрастанию: \n");
for (i = 0; i < N; i ++)
printf("%d ", A [i]) ;
}
1.3.3. Многомерные массивы
В синтаксисе Си++ определены только одномерные массивы. Массивы большей размерности трактуются как массивы массивов. Все особенности использования статических многомерных массивов можно рассмотреть на примере двумерных массивов (матриц).
1.3.3.1. Описание двумерного массива, размещение в памяти, операции
Описание двумерного массива требует, чтобы были указаны два размера: число строк и число столбцов матрицы, например:
int a [2][5]; // Массив из 2-х одномерных массивов,
// в каждом из которых 5 элементов.
При описании массива в памяти выделяется место под размещение его элементов. Элементы двумерного массива размещаются построчно подряд по возрастанию индексов (построчно). Индексы, как и раньше, нумеруются с нуля.
Инициализация двумерных массивов синтаксически выглядит как инициализация нескольких одномерных массивов, например:
int a [3] [4] = {
{1, 2, 3, 4}, // Значения элементов нулевой строки.
{2, 4, 6, 8}, // Значения элементов первой строки.
{9, 8, 7, 6} // Значения элементов второй строки.
};
При размещении в памяти будет выделено место под запись 3 * 4 = 12 элементов, и они будут размещены линейно, как на рис. 1.2.3.1.1, в следующем порядке:
1
|
2
|
3
|
4
|
2
|
4
|
6
|
8
|
9
|
8
|
7
|
6
|
Нулевая строка
|
Первая строка
|
Вторая строка
|
Рис. 1.2.3.1.1. Размещение элементов двумерного массива
Как и одномерные массивы, матрицы могут быть условно переменной длины. При описании матрицы число строк и столбцов задает константное выражение. Это целочисленные значение, которые определяет механизм выделения памяти для матрицы. Реально из выделенного пространства можно использовать меньшее число данных. При этом следует ввести переменные, обозначающие реальное число строк, столбцов (меньшее или равное указанному в описании), которые будут управлять процессом просмотра элементов матрицы.
Операции над двумерным массивом не разрешены, кроме операции доступа к ее элементам (разыменования). Если реальный размер (по описанию) обозначить переменными n, m (матрица имеет n строк и m столбцов) то обращение к элементу массива aij можно выполнить по индексу (прямая адресация) или указателю (косвенная адресация):
a [i][j] // Адресуется элемент массива, стоящий на
// пересечении i - того столбца и j - той строки.
* (a+i * m + j) // Адресуется элемент массива, отстоящий от его
// адреса на i * m + j значений.
При косвенной адресации константа m (количество элементов в строке) определяет разбиение адресного пространства, выделенного для матрицы, на строки определенной длины.
1.3.3.2. Алгоритмы работы с двумерными массивами
Алгоритмы работы с двумерными массивами похожи во многом на приемы работы с одномерными массивами. В большинстве случаев требуется обращение ко всем элементам матрицы, тогда алгоритм просмотра содержит вложенный цикл, у которого во внешнем цикле управляющей переменной служит номер строки матрицы, а во внутреннем – номер столбца. Тогда просмотр элементов происходит построчно.
Пусть есть описание матрицы:
#define n 5
#define m 7
…
int a[n][m];
Здесь размер матрицы определяют define – константы. Для матрицы условно переменной длины следует описать размер по максимуму, и ввести переменные, реально определяющие размер матрицы.
int a[25][25]; // Максимальное число строк, столбцов.
int n, m; // Реальное число строк, столбцов.
…
printf ("Введите размер матрицы (строк, столбцов <25)\n");
scanf ("%d%d", &n, &m); // Теперь это наибольшие значения параметров
// циклов управления.
Для обращения к элементам матрицы по строкам цикл записывается так:
for (i = 0; i<n; i ++)
for(j = 0; j<m; j ++)
{
// В теле цикла обращение к переменной a[i][j].
}
Если переменить эти циклы местами, просмотр будет происходить по столбцам:
for( j = 0; j<m; j ++)
for (i = 0; i<n; i ++)
{
// В теле цикла обращение к переменной a[i][j].
}
Зная, что матрица хранится как одномерный массив, можно ее описать как одномерный массив, а элементы адресовать как элементы матрицы, используя косвенную адресацию, например:
int a[25]; // Матрица 5 на 5.
…
int n, m;
n = 5;
m = 5;
Независимо от способа описания двумерного массива, обращение к его элементам с использованием указателя, выполнит те же действия, что и обращение по индексам, а синтаксически должно быть записано так:
for (i = 0; i<n; i ++)
for(j = 0; j<m; j ++)
{
// В теле цикла обращение к переменной * (a+i * m + j)
}
Как видим, запись сложного цикла нисколько не изменилась, а изменился только синтаксис обращения к переменной. Очевидное преимущество косвенной адресации, это возможность работать с одномерным массивом как с матрицей.
Найдем сумму элементов каждой строки матрицы.
void main(void)
{
int matr[25]; // 5 строк по 5 элементов.
in Sum[5]; // 5 сумм.
int n, m;
int i, j;
// Матрица получит значения, например, случайным образом:
for (i = 0; i < n; i ++)
for(j = 0; j<m; j ++)
* (a+i * m + j) = random (25);
// Вычисление сумм:
for (i = 0, Sum[i] = 0; i < n; i ++)
{
for(j = 0; j<m; j ++) // Цикл накопления суммы в строке.
Sum[i] += * (a+i * m + j);
printf ("Сумма элементов равна %d\n", Sum (matr,n,m) );
}
}
1.3.3.3. Использование функций при работе с двумерными массивами
Использование функций при обработке матриц предполагает два кардинально различных подхода. В первом случае матрица существует или рассматривается как самостоятельная структура данных, к которой необходимо применить какой-либо алгоритм обработки. В качестве параметра такой функции будет использована вся матрица целиком. Чтобы задача обработки решалась бы в общем виде, в функцию следует передать имя матрицы и ее размеры (как параметры функции), при этом функция получает матрицу как двумерный массив. Си++ должен знать, каков способ разбиения этой структуры на строки, поэтому число строк данных можно опустить, а число данных в каждой строке опускать нельзя. Оно должно быть указано обязательно как константное выражение в квадратных скобках при передаче матрицы. Так, прототип функции, получающей матрицу в качестве входного данного, может выглядеть так:
int function (int a [][m], int n, int m); // Здесь m – константное выражение.
В качестве примера рассмотрим функции для ввода и вывода матрицы на экран. Предполагается, что матрица условно переменного размера, поэтому число строк и столбцов матрицы по описанию определено define – константами N = 5 и M = 5. Реальный размер матрицы определяют переменные n, m, значения которых вводятся.
#include <stdio.h>
#define N 5
#define M 5
void input_matr (int a[][M], int &n, int &m); // Важно, что длина строки, это константа.
void print_matr (int a[][M], int n, int m); // Число строк можно не передавать.
// Главная программа описывает входные данные.
void main(void)
{
int n, m; // Реальные размеры матрицы.
int matr [N][M]; // Описан двумерный массив.
input_matr (matr, n, m); // Передан в функцию ввода.
print_matr (matr, n, m); // Предан в функцию вывода.
}
// Описание функции ввода. Параметры функции – имя и размеры массива.
void input_matr (int a[][M], int &n, int &m) // n, m возвращаются по ссылке.
{
int i, j;
printf ("Введите размер матрицы не более %d на %d\n", N, M);
scanf ("%d%d", &n, &m);
printf ("Введите матрицу.\n");
for (i = 0;i < n; i ++)
for (j = 0; j < m; j ++)
scanf("%d", &matr[i][j]);
}
// Описание функции вывода. Параметры функции – имя и размеры массива.
void print_matr (int a[][M], int n, int m)
{
int i, j;
for (i = 0; i < n; i ++)
{
for (j = 0; j<m; j++)
printf ("%5d", mas[i][j]);
printf ("\n"); // Разбиение вывода на строки.
}
}
Во втором случае матрица рассматривается как массив из одномерных массивов, при этом для обработки отдельных строк матрицы можно использовать функции обработки одномерных массивов. Зная, что элементы матрицы, это одномерные массивы, каждый из них можно по очереди передавать в функцию, которая работает с одномерным массивом.
Пусть есть функция вывода одномерного массива и функция преобразования, которая перемещает на первое место минимальный элемент. Чтобы использовать их для работы со строками матрицы, следует передавать в функции строки матрицы поочередно, обычно в цикле.
#define N 5
#define M 5
// Есть функция вывода одномерного массива.
void print_mas (int mas[], int len) // Вывод массива в общем виде
{
for (int i = 0; i < len; i ++)
printf("%5d", mas[i]);
printf("\n");
}
// На ее основе можно описать функцию вывода матрицы как совокупности
// одномерных массивов.
void print_matr (int mas[][M], int n, int m){
printf ("Матрица:\n");
for (int i = 0; i < n; i ++)
print_mas(mas[i],m); // Обращение к функции вывода массива.
}// Есть функция преобразования одномерного массива в соответствии с условием.
void Change (int mas[], int len)
{ // Наименьший элемент перемещается вперед.
int *ip;
int *min = mas;
for (ip = mas; ip < mas+len; ip ++)
if (*ip < *min)
min = ip;
// Перестановка при завершении поиска.
*ip = *min;
*min =*mas;
*mas = *ip;
}
// main должна объявить матрицу и выполнить управление вызовами функций.
void main(void)
{
int n, m;
int matr [N][M]; // Матрица объявлена размером 5 на 5.
int i;
input_matr (matr, n, m); // Передан в функцию ввода матрицы.
// Для преобразования матрицы ее строки передаются в функцию по очереди как
// одномерные массивы. Цикл управления находится в основной программе.
for (i = 0;i < n;i ++)
Change (matr[i], m); // matr[i], это i – тая строка матрицы.
// Функция вывода матрицы вызывается после завершения обработки.
print_matr (matr, n, m);
}
1.3.4. Указатели
Любой объект программы (переменная, массив, функция и прочие) имеет необходимые атрибуты и операции:
1) атрибуты: имя адресует область памяти, тип определяет механизм выделения памяти, значение определяется типом объекта.
2) операции: взять значение по указанному адресу или изменить значение.
Если говорить о механизмах программной реализации объекта, то имя, это адрес области памяти, а значение, это содержимое области памяти, примерно как на рисунке 1.2.3.1.
Рис.1.2.4.1. Соотношение программного и машинного представления объекта
Определение. Указатели – такие объекты (переменные), значением которых являются адреса других объектов (или какой либо области памяти).
Кроме того, значением указателя может быть пустое значение, не равное никакому адресу. Он объявлено константой NULL в нескольких заголовочных файлах, например, <stdef.h>, <stdio.h>.
1.3.4.1. Объявление указателей
Описание переменных типа указатель обязательно должно показывать тип переменной, на которую ссылается (которую адресует) указатель. Это связано с тем, что для хранения переменных разного типа выделяется разный объем памяти. Сама переменная типа указатель имеет всегда одинаковый размер 2 или 4 байта в зависимости от модели памяти.
Признаком того, что в объявлении вводится указатель, является символ '*'.
int *a; // Указатель на ячейку, содержащую целое число.
float *x; // Указатель на ячейку, содержащую действительное число.
char *c; // Указатель на ячейку, содержащую символ.
void *p; // Указатель на ячейку неизвестного типа.
Попробуем проиллюстрировать графически размещение переменных и указателей в памяти, на рис. 1.2.4.1.1.
Пусть объявлены переменные:
char a;
int b;
float c;
…
|
Значение а
|
|
|
|
|
|
|
…
|
|
1 байт
DS:FFF3
|
2 байта
DS: FFF2
|
4 байта
DS:FFEE
|
|
Пусть объявлены указатели:
char *pa;
int *pb;
float *pc;
…
|
|
|
|
|
|
|
…
|
|
адрес char 2 байта
|
адрес int 2 байта
|
адрес float 2 байта
|
|
Рисунок 1.2.4.1.1. Размещение переменных и указателей в памяти.
1.3.4.2. Операции над указателями
Операция получения адреса &
Это унарная операция, которая может быть применена к операнду любого типа. Возвращает 16-ричный адрес объекта (его размещение в памяти).
Например,
int x = 3; // Выделены 2 байта по какому-то адресу и присвоено значение.
int *px; // Выделены 2 байта для хранения адреса данного типа int.
// Операция &x получит адрес переменной х. Его можно присвоить переменной,
// специально предназначенной для хранения адреса, это px.
px = &x; // Адрес можно присвоить указателю
Операция применяется только к именованным объектам, размещенным в памяти. Бессмысленно ее применение к константам, выражениям, внешним объектам.
Операция получения значения по указанному адресу *
Синонимы – операция разыменования, раскрытия ссылки или обращения по адресу. Это унарная операция, операндом может быть только указатель. Возвращаемое значение имеет тип той переменной, на которую показывает указатель, и возвращает значение, размещенное в той области памяти, на которую ссылается указатель.
Например,
int x = 3; // Выделены 2 байта по какому-то адресу и присвоено значение.
int *px; // Выделены 2 байта для хранения адреса данного типа int.
// Операция &x получит адрес переменной х.
px = &x; // Адрес переменной x можно присвоить указателю.
int new_x; // Новая переменная new_x.
new_x = *px; // Переменная new_x получила значение той переменной,
// которая хранится по адресу, имеющему значение px.
// px знает адрес x, значит, new_x получит значение 3.
Тип данного void может адресовать объект любого типа, но к нему нельзя применить операцию *.
Пример недопустимого использования void.
int x = 3;
float y = 2.5;
void *p_k; // Указатель на void.
void *p_y; // Указатель на void.
p_k = &k;
p_y = &y;
Операция присваивания для указателей.
Операция присваивания в С++ имеет особенности, связанные с преобразованием типов. Переменной можно присваивать значение выражения такого же типа, как и переменная, но если переменная и выражение разных типов, то выполняется неявное преобразование при присваивании, которое может привести к потере данных. Чтобы избежать неприятностей, рекомендуется использовать явное преобразование типов.
имя_переменной = (имя_типа) имя_переменной;
Например:
int x;
float y;
y = (int) x;
x = (float) y;
При присваивании указателей возможные ошибки несоответствия типов могут вызвать не только потерю данных, но и привести к трагическим последствиям. Представьте, что происходит, если адрес перепутал почтальон. Тем не менее, преобразование типов указателей может быть выполнено точно так же, как и простых типов, но с использованием операции *, например:
int *x;
float *y;
y = (int *) x;
x = (float *) y;
Механизмы, связанные с таким преобразованием, очень тонкие. Попытаемся показать это на примере.
// Объявим переменную L типа unsigned long и запишем в нее шестнадцатеричную
// long константу 0x12345678L. Объявим три указателя (на char, int и long), и
// каждому из них присвоим адрес этой константы (&L). При присваивании будем
// выполнять приведение типов (например, *c = (char *) &L).
unsigned long L=0x12345678L; // Восьмеричное длинное.
char *c = (char *) &L;
int *i = (int *) &L;
long *l = (long *) &L;
printf ("\nЗначение L = %lx", L); // L = 12345678
printf ("\nАдрес L (то, что &L) = %p", &L); // Например, FFF2.
printf ("\nАдрес с=%p *c= %#x", c, *c); // Адрес = FFF2, значение 0x78
printf ("\nАдрес i=%p *i= %#x", i, *i); // Адрес = FFF2, значение 0x5678
printf ("\nАдрес l=%p *l= %#lx",l, *l); // Адрес = FFF2, значение 0x12345678
Для вывода шестнадцатеричных адресов используется формат %#x или %р. При таком выравнивании адресов переменная меньшего типа получает младшие разряды переменной большего типа. Так, переменная с возьмет значение одного младшего байта переменной L, i возьмет значение двух младших байтов переменной L, а l – всех четырех байт. Распределение памяти показано на рисунке 1.2.4.2.1
.
1
|
2
|
3
|
4
|
5
|
6
|
7
|
8
|
|
|
|
1 байт char
|
|
|
2 байта int
|
4 байта long
|
Рисунок 1.2.4.2.1. Механизм преобразования типов для указателей
Унарные операции ++ и – –.
Унарные операции ++ и – – изменяют значение адреса в зависимости от типа данных, с которым связан указатель, а именно:
Для char на 1 байт.
Для int на 2 байта.
Для float на 4 байта.
Эта операция называется смещение указателя. В следующем примере показано, что выделение адресов в сегменте данных происходит не справа налево, а от старших байт вниз.
int a = 10;
int b = 20;
int c = 30;
int *pa = &a; int *pb = &b; int *pc = &c;
printf ("\nАдрес %p Значение %d", pa, *pa); // FFF4, значение 10.
printf ("\nАдрес %p Значение %d", pb, *pb); // FFF2, значение 20.
printf ("\nАдрес %p Значение %d", pc, *pc); // FFF0, значение 30.
// Операция смещения указателя приведет к просмотру объектов a, b,c , если
// использовать pa – – или pc++.
for( int k = 1; k <= 3; k++)
printf ("\nАдрес %p Значение %d",pa – –, *pa);
// Будет выведено то же, что и в трех предыдущих операторах вывода.
Сложение и вычитание.
Складывать адреса бессмысленно, но можно прибавить к адресу целое число, например, pa + число. Будет получено новое значение адреса, смещенное относительно старого указателя в зависимости от типа указателя, на «число*sizeof (тип_указателя)» в байтах.
Вычитание адресов можно применить к указателю и числу или к двум указателям. В первом случае будет получено новое значение адреса, смещенное относительно старого указателя, как и при сложении. Во втором случае разность (со знаком), это расстояние в единицах, кратных размеру одного объекта указанного типа. Например, смещение pb на +1, это адрес a, смещение pb на –1, это адрес c.
Пример.
printf ("\nАдрес %p Значение %d", pb+1,*(pb+1)); // Значение 10
printf ("\nАдрес %p Значение %d",pb-1,*(pb-1)); // Значение 30
printf ("\nРасстояние между с и a = %d",pc – pa); // Расстояние равно 1.
Операции отношения.
Все операции отношения могут использоваться для сравнения значений адресов. Разрешается сравнивать только указатели одинакового типа или указатель сравнить с константой NULL. Многие функции стандартных библиотек Си++, возвращающие указатели. Если функция не может вернуть адрес, то возвращаемым значением будет NULL.
Приоритеты операций, разрешенных над указателями.
- Унарные операции косвенной адресации * и получения адреса &.
- Аддитивные операции.
- Операции сравнения.
- Операция присваивания.
Операции ++ и – – имеют различный приоритет, наивысший в префиксной форме, например pa++, и наименьший в постфиксной, например pс– –.
1.3.4.3. Указатели и массивы
Синтаксис языка С++ определяет имя массива как адрес его первого элемента (с нулевым значением индекса). Все следующие элементы располагаются подряд в порядке возрастания адресов. Обращение к элементам массива может быть выполнено с помощью операции [ ] (прямая адресация) или с помощью операции * (косвенная адресация).
В первом случае для нумерации элементов массива используются целочисленные индексы, нумерующие элементы массива.
Во втором случае для определения номера элемента внутри массива используется смещение указателя от первого элемента.
Например, если объявлен массив:
int mas [] = {10, 20, 30, 40};
то mas – это адрес первого элемента массива (адрес mas[0]). Адрес следующего элемента mas[1] или mas + 1, следующего mas[2] или mas + 2, и так далее.
Косвенная адресация в массивах.
Пусть объявлен массив
int mas[] = {10, 20, 30, 40};
Пусть его длина определена:
int n = sizeof(mas) / sizeof(int);
Для прямой адресации в массиве используется рабочая переменная, которая определяет целочисленное значение индекса элемента массива:
int i;
...
for ( i = 0 ; i < n; i++)
// обращение к mas[i]
Для косвенной адресации в массиве используется рабочая переменная, которая определяет значение адреса элемента массива:
int *pti; // Тип этого указателя, как у элементов массива (синоним массива).
...
for ( pti = mas; pti < mas + n; pti++)
//обращение к *pti
Обращение к элементам массива при изменении индекса и обращение к элементам массив при смещении указателя показано на рис. 1.2.4.3.1.
int i;
int *pti;
pti = mas;
// Выделение памяти для массива mas:
…
|
1
|
0
|
2
|
0
|
3
|
0
|
4
|
0
|
…
|
|
2 байтa
mas[0] при i = 0
*pti при
pti = mas
|
2 байта
mas[1] при i = 1
*(pti+1) при
pti = mas+1
|
2 байта
mas[2] при i = 2
*(pti+2) при
pti = mas +2
|
2 байта
mas[3] при i = 3
*(pti+3) при
pti = mas +3
|
|
// Выделение памяти для указателя pti:
Рисунок 1.2.4.3.1. Прямая и косвенная адресация в массивах
Так, после присваивания pti = mas, указателю доступен адрес массива.
printf ("\n Первый элемент массива= %d", *pti;)
printf ("\n Второй элемент массива= %d", *++pti); // *(pti+1)
printf ("\n Третий элемент массива= %d", *++pti); // *(pti+2)
printf ("\n Последний элемент массива= %d", *(pti+3));
Приведем пример различной адресации в массиве при вычислении суммы элементов массива. Это обычный алгоритм обработки массива. Для решения задачи необходим последовательный просмотр всех элементов массива.
#define N 5
void main(void)
{
int mas [N] = {1, 2, 3, 4, 5};
int Sum;
int i;
// Прямая адресация. Обращение к элементам массива через операцию [].
for (i = 0, Sum = 0; i < N; i++)
Sum += mas[i];
printf ("\nСумма %d", Sum);
// Косвенная адресация. Обращение к элементам массива через операцию *.
int * pti;
for (pti = mas, Sum = 0; pti < mas + N; pti++)
Sum += *pti;
printf("\nСумма %d", Sum);
// Косвенная адресация. Обращение к элементам массива смещением указателя
// относительно начала массива.
for (pti = mas, i = 0, Sum = 0; i < N; i++)
Sum += *(mas + i);
printf("\nСумма %d", Sum);
// Напечатаем номера и адреса элементов этого массива.
// Спецификатор формата для вывода адресов %р.
int k;
for (ptr = mas, k = 0; ptr < mas+N; ptri++)
printf("\nНомер %d, Адрес %p", k++, pti);
}
1.3.4.4. Динамические массивы (массивы динамической памяти)
При статическом распределении памяти для элементов массива есть существенный недостаток, а именно, общее количество элементов массива должно быть известно при компиляции, когда происходит распределение памяти для элементов массива. Часто это значение неизвестно заранее, и его необходимо определить в процессе выполнения программы.
1.2.4.4.1. Использование механизмов Си для динамического выделения памяти
Для получения массивов с переменными размерами используются указатели и функции библиотеки динамического выделения памяти <alloc.h>.
Библиотека <alloc.h> содержит функции, которые возвращают значения указателей типа void* (для универсализации):
void *malloc (unsigned N);
Возвращает указатель на начало области памяти длиной N байт.
void *calloc (unsighed N, unsigned M);
Возвращает указатель на начало области памяти, необходимой для размещения N переменных по M байт каждый.
void *realloc(void * Имя_блока, unsighed N);
Изменяет размер памяти, ранее выделенной для блока, и имеющей имя «Имя_блока». Возвращает указатель на начало области памяти длиной N байт. Если адрес блока = NULL, то память ранее не выделялась, и функция выполняется как malloc.
Поскольку тип функций void *, то после выделения памяти необходимо явное преобразование к тому типу, который требуется, с помощью операции приведения типа (тип *). Если память не может быть выделена, любая из функций возвращает значение NULL.
Для освобождения памяти используется функция
void * free (void *Имя_блока);
Освобождает память, ранее выделенную для блока. Первый байт блока будет иметь значение NULL.
При динамическом выделении памяти необходимо знать, сколько надо памяти, и сколько есть в наличии. Этот вопрос решат функции:
coreleft ();
Возвращает свободный объем кучи в байтах.
sizeof (имя_типа)
Или
sizeof (имя_переменной_любого_типа)
Возвращает размер памяти в байтах, занятый объектом, или требуемый для размещения данного указанного типа.
Пример. Покажем, как можно определить размер свободного динамического пространства, и как операция sizeof применяется к объектам и именам типов (неважно, как базовых, так и сконструированных).
#define N 4
void main (void)
{
long l; // Объем пространства может быть большой.
int i;
float f;
double d;
int m1[N];
float m2[N];
l = coreleft();
printf ("\nСвободно байт= %ld", l);
printf ("\nБайт для int =%d", sizeof (i));
printf ("\nБайт для float =%d", sizeof (f));
printf ("\nБайт для double =%d", sizeof (d));
printf ("\nБайт для int массива =%d", sizeof (int)*N);
printf ("\nБайт для float массива =%d", sizeof (float)*N);
printf ("\nБайт для float массива =%d", sizeof (m2));
}
Пример. Покажем пример создания одномерного динамического массива, длина которого будет определена только при выполнении программы, соответственно, после этого будет выделена память. Адресация не зависит от способа выделения памяти, и может быть как прямой, так и косвенной.
#include <alloc.h>
#include <stdio.h>
void main(void)
{
int *mas; // Указатель на массив.
int i, N;
int *ip;
printf("Введите количество элементов ");
scanf("%d", &N);
mas = (int *) malloc(N * sizeof(int)); // Выделение памяти для массива.
if (mas == NULL)
{
printf("\nНет памяти");
return;
}
// Массив существует. Адресация массива выполняется через имя указателя.
// Адресация по индексу.
printf ("Введите %d элементов:\n ", N);
for (i = 0; i < N; i++)
scanf("%d", &mas[i]);
printf("\n");
// Адресация по указателю.
for (ip = mas; ip < mas + N; ip ++)
printf("%5d", *ip);
// Высвобождение памяти.
free (mas);
}//
Важное примечание. При выделении памяти переменной (массиву) через указатель, переменная (массив) не имеет собственного имени. Обращение к переменной (массиву) выполняется только посредством указателя, который играет роль синонима имени.
Пример.
int *k;
k = malloc (int); //Переменная существует, но не имеет имени.
*k =10; //Запись значения в память по адресу, на который ссылается k.
Объект динамической памяти разрушается при использовании функции free или при завершении работы программы, если не был разрушен ранее.
1.2.4.4.2. Использование механизмов Си++ для динамического выделения памяти
Для динамического выделения памяти в С++ введены специальные операции выделения динамической памяти new и высвобождения delete. Операция new выделяет в куче (heap) память требуемого размера для объекта, операция delete разрушае объект, возвращая память в кучу.
Синтаксис:
Указатель = new Имя_объекта [ количество_элементов ];
delete имя_объекта;
Операция new пытается открыть объект с именем Имя_объекта путем выделения sizeof (Имя_объекта) байт в куче. Операция delete удаляет объект с указанным именем, возвращая столько же байт в кучу.
Объект существует с момента создания до момента разрушения или до конца программы.
Пример:
name *ptr; // Здесь name любой тип, кроме функции.
...
if (!(ptr = new name)) // Может оказаться равным NULL.
{
printf("Нет места в памяти.\n");
exit ();
}
// Использование объекта по назначению
...
delete ptr; // Разрушение объекта.
Пример создания динамического массива.
int *mas; // Указатель на массив.
…
printf ("Введите количество элементов ");
scanf ("%d", &N);
mas = new int [N]; // Выделение памяти для массива.
if (mas == NULL)
{
printf("\nНет памяти");
return;
}
// Массив существует. Адресация массива выполняется через имя указателя.
…
delete mas;
// Массив разрушен.
Объект динамической памяти разрушается при использовании операции delete или при завершении работы программы, если не был разрушен ранее.
1.2.4.4.3. Динамические двумерные массивы
Объявляются как указатель на указатель. Память выделяется для каждой строки отдельно, как и высвобождается. Адресация не зависит от способа выделения памяти, и может быть как прямой, так и косвенной. Размещение в памяти для каждого массива линейное, примерная схема показана на рис.1.2.4.5.3.1.
Имя массива
a
|
|
Имя строки
a[i]
|
|
Имя элемента
a[i][j]
|
|
|
|
|
|
|
|
a
|
|
a[0]
|
|
a[0][0]
|
…
|
a[0][M]
|
|
|
|
|
|
|
|
a[1]
|
|
a[1][0]
|
…
|
a[1][M]
|
|
|
|
|
|
|
|
|
…
|
|
|
…
|
|
|
|
a[N–1]
|
|
a[N–1][0]
|
…
|
a[N–1][M]
|
|
|
|
|
|
|
Рис. 1.2.4.5.3.1. Схема выделения памяти для динамического двумерного массива.
Приведем пример кода программы, которая выделяет память для динамического двумерного массива, а затем высвобождает ее.
#include <alloc.h>
…
void main (void)
{
int **a; // Указатель на массив указателей (на строки матрицы).
int N, M; // Размер матрицы.
int i, j;
int *ptr;
printf ("Введите число строк\n");
scanf ("%d", &N);
printf ("Введите число столбцов\n");
scanf ("%d", &M);
// Первый этап: выделение памяти для массива указателей на строки матрицы.
// Требуется N элементов размером указателя на int.
a = (int **) calloc (N, sizeof (int *)); // Для массива строк.
if (a == NULL)
{
printf("\nНедостаточно памяти");
exit (1);
}
// Второй этап: выделение памяти для каждой строки матрицы.
// Выполняется N раз, выделяется по M элементов.
for (i = 0; i < N; i++)
{
a[i] = (int *) calloc (M, sizeof (int)); //Для каждой строки
if (a[i] == NULL)
{
printf ("\nНедостаточно памяти");
exit (1);
}
}
// Адресация в динамическом двумерном массиве может быть
// прямой или косвенной.
// Прямая адресация:
printf ("Введите матрицу размером %d строк и %d столбцов\n", N, M);
for (i = 0; i < N; i++)
for (j = 0; j < M; j++)
scanf("%d", &a[i][j]);
…
printf ("\nРезультат");
for (i = 0; i < N; i++)
{
printf("\n");
for (j = 0; j < M; j++)
printf ("%d ", a[i][j]);
}
// Высвобождение памяти в два этапа.
for (i = 0; i < N; i++)
free (a[i]); // Разрушаются строки.
free(a); // Высвобождается указатель на матрицу.
}
Использование динамических двумерных массивов в функциях имеет некоторые особенности, которые иллюстрируются следующим примером.
#include <stdio.h>
#include <alloc.h>
// Покажем на примере функций ввода и вывода матрицы.
void print_matr_din (int **mas, int N, int M);
int ** get_matr (int &N, int &M);
void main (void)
{
int N, M;
int ** matr;
//Функция выделения памяти для матрицы и ввода значений. Разбивает на строки.
matr = get_matr (N, M);
// Функция вывода матрицы.
if (matr != NULL)
print_matr_din (matr, N, M);
}
// Описание функций.
void print_matr_din (int **a, int N, int M)
{
int i, j;
for (i = 0; i< N; i++)
{
for (j = 0; j<M; j++)
printf("%5d", a [i][j]);
printf("\n");
}
}
// Получение матрицы динамическое.
// Функция возвращает указатель на матрицу и способ разбиения на строки.
int ** get_matr (int &N, int &M)
{
int **a, i, j;
int *ptr;
printf ("Введите число строк и столбцов.\n");
scanf ("%d%d", &N, &M);
a = (int **) calloc(N, sizeof(int *)); //Для массива строк
if (a == NULL)
{
printf("\nНедостаточно памяти");
return NULL; // Выход из функции.
}
for (i = 0; i < N; i++)
{
a[i] = (int *) calloc(M, sizeof (int)); //Для каждой строки
if (a[i] == NULL)
{
printf("\nНедостаточно памяти");
return NULL; //Выход из функции
}
}
printf ("Введите матрицу %d на %d)", N, M);
for (i = 0; i < N; i++)
for (j = 0; j < M; j++)
scanf("%d", &a[i][j]);
return a;
}
1.3.4.5. Массивы указателей
Массивы указателей могут использоваться для хранения адресов объектов и управления ими. Эта возможность приобретает значение, когда объекты большого размера, или когда они имеют изменяемый размер (динамические объекты).
Синтаксис определения массива указателей:
Тип_массива * Имя_массива [количество_элементов];
Или с одновременной инициализацией:
Тип_массива * Имя_массива [] = {инициализатор};
Тип_массива * Имя_массива [количество_элементов] = {инициализатор};
Здесь «Тип_массива» может быть базовым или производным, а инициализатор, это список значений адресов типа «Тип_массива *».
Например:
int Data [6]; // Массив целочисленных значений.
int *pD [6]; // Массив указателей.
int *pI [ ] = { &Data[0], &Data[1], &Data[2]); // С инициализацией.
Здесь каждый элемент массивов pD и pI является указателем на объекты типа int, следовательно значением каждого элемента pD[i] и pI[i] может быть адрес объекта типа int. Элементы массива pD не инициализированы, значит, имеют неопределенные адреса. В массиве pI три элемента, значения которых, это адреса конкретных элементов массива Data.
Возможности массива указателей проявляются тогда, когда его элементы адресуют либо элементы другого массива, либо указывают на начало одномерных массивов соответствующего типа. В таких случаях красиво решаются задачи сортировки сложных объектов с разными размерами. Например, можно ввести массив указателей, адресовать с помощью его элементов строки матрицы и затем решать задачи упорядочения строк матрицы, не переставляя строки двумерного массива, представляющего матрицу.
В качестве простого примера рассмотрим программу сортировки по возрастанию одномерного массива без перестановки его элементов. Алгоритм сортировки массива указателей тот же, что и алгоритм сортировки массива, рассмотренный в разделе 1.2.2.6. Здесь же элементы исходного массива не меняются местами, то есть остаются в прежнем порядке, а сортируется массив указателей на элементы массива pA[N]. С его помощью определяется последовательность просмотра элементов массива A[N] в порядке возрастания их значений.
Рис. 1.2.4.4.1 иллюстрирует исходную и результирующую адресации элементов массива A элементами массива указателей *pA.
int A
|
10
|
9
|
8
|
7
|
6
|
5
|
4
|
3
|
2
|
1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int *pA
|
|
|
|
|
|
|
|
|
|
|
|
а – массивы до сортировки
int A
|
10
|
9
|
8
|
7
|
6
|
5
|
4
|
3
|
2
|
1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int *pA
|
|
|
|
|
|
|
|
|
|
|
|
б – массивы после сортировки
Рис. 1.2.4.5.1. Массивы в задаче сортировки
А теперь приведем текст примера.
#define N 10
void main (void)
{
int A [N] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
int * pA [N] ;
int * buf; // Буфер для перемены адресов.
int i, j;
for (i = 0; i < N; i++)
pA[i]=&A[i]; // Копирование адресов (Рис.1.2.4.4.1.а)
for (i = 0; i < N – 1; i ++)
for (j = i + 1; j < N; j ++)
{
if (*pA[i]>*pA[j])
{ // Перестановка в массиве адресов.
buf = pA [i] ;
pA [i] = pA [j] ;
pA [j] = buf;
}
}
printf ("\n По возрастанию: \n");
for (i = 0; i < N; i ++)
printf ("%d ", *pA [i]); // Выведет по возрастанию.
printf ("\n По возрастанию: \n");
for (i = 0; i < N; i ++)
printf("%d ", A[i]) ; // Выведет в исходном порядке.
}
1.3.4.6. Указатели и функции
Механизмы указателей при работе с функциями используются для решения многих практических задач.
1.2.4.6.1. Передача параметров по адресу.
Механизм передачи параметров в функцию по значению рассмотрен ранее. При обращении к функции и передаче ей параметров создается локальная копия объекта в теле функции. Значение объекта вызывающей программы, передаваемое в функцию как фактический параметр, не может быть изменено в теле функции.
Чтобы определить функцию, возвращающую более одного значения, нужно дать ей возможность изменить значения параметров. В качестве параметра следует передавать не значение объекта вызывающей программы, а его адрес. При этом используется механизм указателей. Адресная операция (&) применяется к имени параметра. Синтаксис этого описания:
тип_ функции имя_функции (тип_параметра & имя_параметра)
Механизм этого способа передачи данных заключается в том, что функция и вызывающая программа работают с адресом объекта в памяти, следовательно, обе могут изменить значение этого данного. При этом и в вызывающей программе и в функции этот параметр должен быть именованным объектом.
В классическом С этот механизм отсутствует, и при передаче параметра по адресу в теле функции необходимо разыменование переменной (операция *). Этим устаревшим способом пользоваться не следует, хотя можно привести пример, чтобы показать, как он нелогичен. Использование этого механизма отчетливо видно в функции стандартного ввода scanf.
// Передача параметра по адресу в стиле С.
void change_5 (int * ptr) // Формальный параметр – указатель.
{
*ptr += 5; // Следует разыменовать.
}
void main(void)
{
int x = 5;
printf("x = %d\n ", x);
change_5 (&x); //Фактический параметр - адрес.
printf("А теперь %d\n ",x);
}
В С++ признак передачи параметра по ссылке указывается в списке формальных параметров при описании функции и в прототипе.
// Передача параметра по адресу в стиле С++.
void change_5 (int &ptr) // Формальный параметр – адрес объекта.
{
ptr += 5;
}
void main(void)
{
int x = 5;
printf("x = %d\n ",x);
change_5 (x);
printf("А теперь %d\n ",x);
}
Приведем еще один пример функции, где необходима передача параметров по адресу. Это функция, которая должна переменить местами значения двух переменных.
// Функция Swap1 с параметрами по значению.
void Swap1 (int x, int y)
{
int tmp;
tmp = x;
x = y;
y = tmp; // Локальные переменные x ,y переменились значениями.
}
// Функция Swap2 с параметрами по ссылке.
void Swap2 (int &x, int &y)
{
int tmp;
tmp = x;
x = y;
y = tmp; // Объекты, расположенные по адресам x ,y
// переменились значениями.
}
1.2.4.6.2. Массивы как параметры функций
Особенность массива в том, что он является указателем. Имя массива, это адрес его нулевого байта. Когда параметром функции является массив, то, согласно синтаксису Си++, в функцию передается его адрес, следовательно, функция будет изменять элементы массива, так как включается механизм передачи данных по адресу.
Способов записи формальных параметров массивов в заголовке функции может быть два:
тип_функции имя_функции (тип_массива имя[])
{
... // Обращение к элементам массива.
}
тип_функции имя_функции (тип_массива * имя )
{
... // Обращение к элементам массива.
}
Как видим, длина массива отсутствует в описании формального параметра. Конструкции Имя[ ] и * Имя различны внешне, но одинаковы по выполняемым механизмам, если имя именует массив. Длина массива, как правило, определена его описанием или передается в функцию как параметр.
В теле функции элементы массива адресуются любым способом независимо от синтаксиса формального параметра. При обращении к функции указывается только имя массива.
Несомненным достоинством этого механизма является возможность простой и надежной реализации алгоритмов обработки массивов переменной длины. При этом задачи обработки массивов решаются в общем виде. В качестве параметров в функцию следует передать имя массива и его длину. Если содержимое массива будет изменено функцией, это изменение доступно и вызывающей программе. Если функция не изменяет длину массива, ее следует передать как параметр-значение, если изменяет, то как параметр-адрес, например:
int function (int a [], int n); // а – имя массива, n – длина массива.
int function (int a [], int &n); // а – имя массива, n – длина массива.
Независимо от способа передачи параметра – массива, фактическим параметром при обращении к функции должно быть имя массива.
Для двумерных массивов задача не может быть решена так просто, потому что двумерный массив при передаче в функцию должен быть разбит на строки. Функция должна знать, каков способ разбиения на строки, поэтому число строк данных можно опустить, а число данных в каждой строке опускать нельзя. Оно должно быть указано обязательно как константное выражение в квадратных скобках при передаче матрицы. Так, прототип функции, получающей матрицу в качестве входного данного, может выглядеть так:
int function (int a [][m], int n, int m); // Здесь m – константное выражение.
Примером использования массивов в качестве параметров функций могут служить функции ввода и вывода одномерных массивов. Эти функции решают алгоритмические задачи в общем виде, и как составляющие личной библиотеки функций, могут использоваться всю оставшуюся жизнь.
Пример.
#define N 40
// Функция вывода массива произвольной длины.
void print_mas (int mas[], int len) // Параметры – имя и длина массива.
{
int i;
printf ("Массив:\n");
for (i = 0; i < len; i++)
printf("%5d", mas[i]);
printf("\n");
}
// Функция ввода массива произвольной длины.
void input_mas(int *mas, int &len) // Параметры – имя и длина массива.
// Длина передается по ссылке.
int *ip;
printf ("Введи количество элементов массива < %d\n", N); // Наибольшая длина.
scanf ("%d", &len);
printf ("Введи элементы массива \n");
for (ip = mas; ip < mas + len; ip ++)
scanf("%5d", ip); // Признак & не нужен.
}
// Функция преобразования массива.
void transform_mas(int *mas, int len) // Параметры – имя и длина массива.
{
int i;
for (i = 0; i < len; i++)
mas[i]=mas[i] *2;
}
В вызывающей программе массив должен быть объявлен, может получить значения. В обращении к функции, чтобы передать массив, нужно указать только его имя.
void main(void)
{
int a1[N] ,a2[N]; // Объявлены два массива.
int n1, n2; // Их длины.
input_mas (a1, n1); // Каждый массив введен, преобразован,
// выведен на печать.
transform_mas (a1, n1);
print_mas (a1, n1);
input_mas (a2, n2);
transform_mas (a2, n2);
print_mas (a2, n2);
}
Стоит обратить внимание, что главная программа теперь занимается только управлением и передачей данных. Логика ее работы прозрачна, текст не перегружен подробностями алгоритмов.
1.2.4.6.3. Функции, возвращающие адреса объектов
Такие функции используются при работе с динамическими данными, то есть массивами и строками. Тип функции (возвращаемого ею значения) может быть одним из базовых типов или указателем. Во втором случае возвращаемое значение, это адрес какого-либо объекта в памяти. Сам объект может иметь произвольный тип, и может быть объектом базового, но чаще конструируемого типов. В случае динамических данных функция может породить новый объект, и вернуть его адрес.
Возвращаемое функцией значение должно быть присвоено переменной типа указатель.
Такими функциями являются многие библиотечные функции, которые требуют выделения памяти под вновь созданный объект, и, как правило, возвращают вновь созданное значение. Например, многие функции работы со строками текста библиотеки <string.h>, многие функции работы с динамической памятью библиотеки <alloc.h>, многие функции ввода данных библиотек <stdio.h> и <stdlib.h> и другие.
Пример функции, возвращающей указатель.
// Пример для простой переменной.
int * func (int n)
{
int * a; // Объявлен указатель.
*a = n; // Его значение равно значению параметра n.
return a; // Возвращается адрес.
}
void main (void)
{
int b;
b = *func (12); // Простая переменная b получает значение,
// возвращенного функцией адреса.
printf("%d", b);
int *d;
d = func (12); // Указатель d получает значение адреса,
// который вернула функция.
printf ("%d", *d);
}
Второй пример показывает использование объекта динамической памяти в функции, возвращающей указатель. Функция имеет два параметра. Если первый параметр больше второго, нужно создать новый объект и присвоить ему значение 1. В противном случае функция не создает объект, и должна вернуть пустой адрес NULL. Возвращаемое значение, это адрес вновь созданного объекта или NULL. Тип функции - указатель на целое.
int * New_obj (int a, int b)
{
int * Obj; //Новый объект будет создан или нет.
if (a>b)
{
Obj = new int; // Создается новый объект.
* Obj = 1; // Его значение 1.
return Obj; // Возвращается его адрес.
}
else
return NULL;
}
void main (void)
{
int p1,p2; // Переменные.
int * ip; //Указатель на новый объект.
printf ("Введите две переменные\n");
scanf ("%d%d",&p1,&p2);
ip = New_obj (p1,p2);
if (ip != NULL)
printf ("Новый объект имеет адрес %p, значение %d\n", ip, *ip);
else
printf("Новый объект не создан, использовать ip нельзя, \
его значение %s\n", ip);
}
1.2.4.6.4. Указатели на функции
Определяя функцию, мы сказали, что функция, это самостоятельный именованный алгоритм решения некоторой законченной задачи, то есть некоторая программная единица, которая обменивается данными с окружающим миром через набор параметров и через возвращаемые значения. Далее мы заявили, что в синтаксисе Си++ функция является одним из конструируемых типов данных, и, как объект программы, имеет имя и имеет тип. Необходимость в таком типе связана с задачами, когда некоторая общая задача решается одинаковым образом для многих частных случаев, которые можно описать в виде отдельных однотипных алгоритмов. При этом функция (или ее адрес) должна выступать в качестве параметра другой функции или в качестве значения, возвращаемого другой функцией.
Оператор-выражение для вызова функции имеет вид:
Имя_функции (список_фактических_параметров)
Здесь в качестве имени до сих пор мы использовали идентификатор, а на самом деле Имя_функции, это указатель на функцию, возвращающую значение конкретного типа. В соответствии с синтаксисом Си++ указатель на функцию, это выражение или переменная, используемые для представления адреса функции. Указатель на функцию содержит адрес первого байта или первого слова ее исполнимого кода (арифметические операции над указателями на функции запрещены).
Самый употребительный указатель на функцию, это ее имя (идентификатор), именно так указатель на функцию вводится в ее описании и в прототипе:
тип Имя_функции (список_формальных_параметров)
{
тело_функции
}
Имя_функции в ее описании и в прототипе – константный указатель. Он навсегда связан с описываемой функцией и не может быть изменен.
Указатель на функцию можно ввести как переменную величину, безотносительно к описанию какой-либо конкретной функции. Синтаксис этого описания:
тип (*Имя_указателя) (список_формальных_параметров);
Здесь все как обычно, кроме (*Имя_указателя), это произвольный идентификатор, вводящий указатель как переменную, которая может быть настроена на адреса конкретных функций.
Например, запись int (*Function) (void);
определяет указатель – переменную с именем Function на некоторое множество функций без параметров, возвращающих значения типа int.
Круглые скобки обязательны в определении указателя на функции. Запись
int * Function (void) ;
будет не определением указателя, а прототипом функции без параметров с именем Function, возвращающей значения типа int*.
После такого определения указателя, Function является переменной, т.е. ей можно присваивать значения других указателей, определяющих адреса других функций программы. Тип указателя переменной должен полностью соответствовать типу функции, адрес которой ему присваивается.
Если переменную – указатель на функцию настроить на адрес конкретной функции, то ее можно использован для вызова этой функции. Таким образом, при обращении к функции можно использовать:
- имя_функции;
- указатель на переменную того же типа, значение которого равно адресу функции;
- выражение разыменования такого же указателя с таким же значением.
Следующий пример иллюстрирует все три способа вызова функций.
void fl (void)
{
printf ("\n Функция fl( ) ") ;
}
void f2 (void)
{
printf ("\n Функция f2( )");
)
void main (void)
{
void (*Function) (void);
// Function – переменная указатель на функцию
f2 (); // Явный вызов функции f2()
Function = f2; // Настройка указателя Function на f2()
(*Function) (); // Вызов f2() по адресу с разыменованием указателя
Function = fl(); // Настройка указателя Function на f1()
(*Function) () ; // Вызов fl() по адресу с разыменованием указателя
Function (); // Вызов fl() без разыменования указателя
}
Результат выполнения программы:
Функция f2 ()
Функция f2 ()
Функция f1 ()
Функция fl ()
Во всех случаях тип указателя должен соответствовать типу вызываемой функции, и между формальными и фактическими параметрами должно быть соответствие.
При определении указателя на функции он может быть инициализирован. В качестве инициализирующего выражения должен использоваться адрес функции того же типа, что и тип определяемого указателя, например:
int F_char (char);
int (*pchar) (char) = F_char;
Массивы указателей на функции.
По смыслу не отличаются от массивов других объектов, их описание:
тип (*Имя_массива [размер]) (список_формальных_параметров);
Например:
int (*pchar [4]) (char);
Здесь pchar – массив указателей на функции, каждому из которых можно присвоить адрес определенной выше функции int F_char (char), и адрес любой функции с прототипом вида
int Имя_функции (char);
Массив функций создать нельзя, но можно описать массив указателей на функции. Тем самым появляется возможность создавать «таблицы переходов», с помощью которых удобно организовывать ветвления с возвратом по результатам выполнения некоторых условий, что используется, например, при создании меню.
Указатели на функции как параметры других функций
Используются, чтобы создавать функции, реализующие в общем виде тот или иной метод обработки других функций, которые могут изменяться. Например, построение графика функции, вычисление интегралов, рядов и прочие. Одним из параметров таких функций должен быть указатель на функцию, для которой выполняется решение. В качестве фактического параметра при обращении в функцию передается адрес конкретной функции, для которой выполняется обработка.
Например:
void Graph (float (*Function)(float),float x0, float xn)
{
// Построение графика функции Function от x0 до xn
}
В основном тексте следует описать множество функций, которые могут быть переданы в функцию Graph, и использовать их имена как фактические параметры при обращении. Их тип и сигнатура параметров совпадают с опрсанием первого параметра функции Graph.
float f1(float x)
{
return sin(x)+cos(x);
}
float f2(float x)
{
return pow(exp(x),–sin(x);
}
void main(void)
{
// Построение графика первой функции.
Graph (f1, –M_PI, M_PI);
// Построение графика второй функции.
Graph (f2, 0., 2*M_PI);
}
1.3.5. Обработка текстовой информации
Текстовая информация, это символы (константы), переменные символьного типа (char), и строки, для которых специального типа данных нет.
1.3.5.1. Символьные константы и символьные переменные
Символьная константа, это лексема, которая состоит из изображения символа и ограничивающих апострофов, например: '*' '?' 'f' 'Я' '~' '#' '1'. Внутри апострофов записывается любой символ, изображаемый на экране в текстовом режиме.
Для объявления объектов символьного типа используются ключевые слова char или unsigned char.
Данные символьного типа занимают один байт. Внутреннее представление символьного данного, это его целочисленный код. Для кодирования символов используется код ASCII (число в пределах от 0 до 256). Первые 32 символа управляющие. Они не имеют графического отображения на экране, а для их представления в тексте программы требуется два символа, первый из которых слэш: '\n', '\\' и т.д.
Такое представление позволяет обращаться к символам как к числовым величинам, что дает некоторые преимущества, а именно:
1. Можно использовать все операции отношений для того, чтобы сравнивать символы друг с другом. При этом сравниваются коды символов, например:
char c1, c2;
c1 = getchar(); // Функция getchar получает символ с клавиатуры.
c2 = getchar();
// Любую операцию отношения можно использовать для сравнения.
int k = c1 = = c2; // k = 1, если символы равны, k = 0, если нет.
k = c1 > c2; // k = 1 , если код с1 больше кода с2, иначе 0.
// Для проверки условия «символ с1 является цифрой», можно использовать то, что
// коды цифр упорядочены по возрастанию.
k = (c1 >= '0' && c1 <= '9')
2. При выполнении арифметических операций над символами операции выполняются над значениями внутренних кодов символов. Можно применять арифметические операции, например, +, –, ++, – –, чтобы получить код символа.
3. Можно использовать символьные переменные как управляющие переменные, чтобы организовать циклы обработки символьных или строковых данных, например, перебор в алфавитном порядке:
char c;
for (c = 'a'; c <= 'z'; c++)
{
...// Например, сравнение символа со значениями с.
}
Здесь символьная переменная c является операндом арифметической операции ++, выполняемой над значением ее внутреннего кода.
Коды символов иногда знать необходимо. Например, внешнее управление программой реализуется с использованием стандартных клавиш, например, стрелок, Enter, Esc, F1 – F12 и пр. Для того, чтобы узнать код клавиши, можно использовать функцию bioskey библиотеки <bios.h>. Эта функция ожидает ввод с клавиатуры значения символа, и имеет три способа обращения. Если фактический параметр равен 1, функция просто ожидает нажатия любой клавиши, что используется для организации бесконечного цикла ожидания события. Если фактический параметр равен 0, функция читает символ, и если равен 2, то функция читает символ с определением регистра (нажата ли клавиша Shift).
Пример.
#include <bios.h> // Для bioskey.
#define ESC 0x11b // 16-ричный код клавиши Esc.
void main(void)
{
int key;
printf ("Press any key…"\n);
while (bioskey(1) – – 0); // Ждет нажатия
do
{
key = bioskey(0); // Читает значение и анализирует его.
printf ("Шестнадцатеричный код %#0x\n", key);
}
while (key != ESC); // Обрабатывается событие «Нажатие Esc».
}
1.3.5.2. Строковые переменные и строковые константы
Формально строки не относятся к константам языка Си+, а представляют отдельный тип его лексем. Строковая константа, это последовательность символов, заключенная в двойные кавычки " ".
Управляющие символы (Esc-последовательности) входят в состав текстовой строки, вызывая управляющее воздействие.
Примеры символьных констант:
"Cтрока символов".
"cлэш запишем как \\ , новую строку как \n"
" ""апостроф"" повторяем дважды."
Внутри длинных строк комбинация \n означает переход на новую строку.
1.2.5.2.1. Механизм представления строки
Строка символов рассматривается как массив символов (типа char[]). Каждый символ хранится в отдельном байте, включая специальные символы. В конце строки должен быть нулевой байт '\0', как признак конца строки. Добавляется автоматически при инициализации строки, при вводе строки. Общее число символов в строке определяется с учетом нулевого символа, и равно «длина строки +1». При формировании строки вручную необходимо заботиться о том, чтобы этот символ был добавлен. Так, длина константы 'W' равна одному байту, а длина строки "W" равна двум байтам.
Операции с массивом в Си++ не допускаются, следовательно, и для строк в целом операций нет. Символьная строка в выражении – это адрес массива символов. Указатель на строку может быть использован везде, где можно использовать указатель. Конечно, не в левой части присваивания.
1.2.5.2.2. Объявление и инициализация строковых переменных
Объявить строковую переменную можно двумя способами.
1. Как статический массив. Считается, что это плохой стиль, так как длина строки может изменяться ограниченно. На самом деле при обработке текстов, так или иначе, ограничение на длину строки существует, и оно может быть достаточно расплывчатым.
char Str [80]; // Длина не более 79 символов.
2. Как указатель. При этом создается динамическая строка данных char, для хранения которой необходимо выделение памяти.
char * Str;
Str = new char [80]; // Как видим, длина строки и здесь определена числом.
При инициализации, независимо от способа объявления строки, всегда происходит выделение памяти под запись значений строки.
char *Str1 = "строка 1"; // Длина строки 9 байт.
char Str2[9] = "строка 2"; // Длина строки 9 байт, как объявлено.
char Str1[] = "строка 3"; // Специальный инициализатор.
char Err[4] = "ошибка"; // Длина строки больше, чем объявлено.
char Err[10] = "не ошибка";// Длина строки меньше, чем объявлено,
// остальные символы заполнены нулями.
1.3.5.3. Ввод-вывод символов и текстовых строк.
Для ввода и вывода текстовой информации и инструментов довольно много, это функции библиотеки <stdio.h>.
1. Форматированный ввод-вывод символов и строк.
Используются функции форматированного ввода printf, scanf. Управляющие форматы для символов %c, для строк %s. При вводе строка вводится только до первого пробела.
2. Ввод-вывод символов.
Используются функции getchar, putchar. Функция getchar() читает из входного потока по одному символу за обращение. Чтение данных начинается после нажатия клавиши Enter. Функция putchar(символ) выводит символьное значение в стандартный выходной поток, обычно это экран.
3. Ввод-вывод строк.
Используются функции gets, puts. Функция gets(строка) читает из входного потока последовательность символов до Enter. Чтение начинается после нажатия клавиши. Функция puts(строка) выводит строку в стандартный выходной поток.
Прототипы функций:
char *gets (char *Str); // Вернет NULL при ошибке.
int puts (const char * Str) // Вернет EOF при ошибке.
Пример.
#include <stdio.h>
void main(void)
{
char Str[80];
printf ("Введите строку\n");
gets (Str);
printf("Введена строка:\n");
puts (Str);
}
Следующий пример покажет, как можно вводить строки при работе с текстом как с массивом строк.
#include <stdio.h>
void main(void)
{
char text [3][20];
for (int i = 0; i < 3; i++)
gets (text[i]);
}
1.3.5.4. Строки и указатели.
Особенности строк связаны с представлением их в памяти как массивов однобайтовых переменных. Соотношение строк и указателей такое же, как соотношение строк и массивов, но в конце строки есть нулевой байт '\0'. Для статических строк память выделяется на этапе компиляции программы, для динамических строк при выполнении программы, но в любом случае массив должен где-то разместиться. Пусть есть два описания:
char Str1[20] ; // Массив, в который можно записать строку.
char *Str2; // Указатель, с которым можно связать строку.
Для статического массива Str1 память будет выделена автоматически при обработке описания компилятором. Строке, с которой можно связать указатель * Str2, память не выделяется, поэтому, если далее следуют операторы:
gets (Str1); // Ошибки нет.
gets (Str2); // Ошибка.
то первый из них допустим, а второй приведет к ошибке времени выполнения программы, так как gets вводит строку в участок памяти с неизвестным адресом. Перед использованием строки следует связать адрес области памяти, в котором может разместиться строка, с указателем Str2. Во-первых, переменной Str2 можно присвоить адрес уже определенного символьного массива. Во-вторых, можно выделить динамическую память в требуемом размере с помощью операции new, а затем полученный адрес связать с указателем Str2. Например:
Str2 = Str1; // Указатель Str2 адресует строку Str1.
Str2 = new char [80];
Во втором случае выделен блок памяти 80 байт, и связан с указателем Str2. Строка не имеет собственного имени, а имя указателя Str2 является его синонимом.
Символьная строка, встреченная в выражении (например, Str1), это адрес массива символов. Может встречаться везде, где можно использовать указатель. Для доступа к отдельным символам строк используются обычные механизмы прямой или косвенной адресации.
Для прямой адресации используются индексы элементов массива. Выделим динамическую память для строки Str2, и выполним копирование в нее исходной строки Str1 в цикле do, управляемом индексом. Реальное число символов в исходной строке, чаще всего, неизвестно, поэтому используется цикл с выходом по событию «достигнут конец строки». Нулевой символ должен быть перенесен в новую строку.
char Str1[80] = "Первая строка.";
char * Str2;
Str2 = new char [80];
// Оператор do выполняет посимвольное копирование,
// перенесет нулевой байт в новую строку.
i = 0;
do
Str2[i] = Str1[i];
while ( Str1[i ++] !='\0'); // Проверка достижения конца строки.
puts (Str2);
Для косвенной адресации используются указатели типа char * в качестве переменных, управляющих обработкой строки. Текущий указатель будет адресовать ровно один символ строки, но в отладчике будет видна оставшаяся часть строки до нулевого байта '\0'. Покажем на том же примере.
char *pts1, *pts2;
Это рабочие переменные для косвенной адресации. Управляет циклом переменная pts1. Ее начальное значение равно адресу исходной строки Str1. *pts1, это значение очередного символа строки, pts++ смещает указатель. Выход из цикла происходит, когда найден признак конца строки.
char Str1[80] = "Первая строка.";
char * Str2 = new char [80];
char *pts1, *pts2;
pts1 = Str1; // Подготовка цикла, это настройка указателей.
pts2 = Str2;
do
*pts2 ++ = *pts1; // Сначала присваивание, затем смещение.
while (*pts1 ++ != '\0'); //
puts (Str2);
В стиле Си++ этот цикл выглядит так:
pts1 = Str1;
pts2 = Str2;
while (( *pts2++ = *pts1++) !='\0');
puts(Str2);
Большинство ошибок при работе со строками связаны с механизмами выделения памяти для динамических строк. Если при объявлении указателя текстовая строка проинициализирована, то выделена память, равная длине этой строки, и нельзя пытаться записать в эту строку больше символов, чем выделено. Если же при объявлении указателя текстовая строка не проинициализирована, то память под запись строки вообще не выделена, и этот указатель можно использовать только как рабочую переменную для косвенной адресации при работе с какой-либо строкой.
Еще раз обратим внимание на отличие способов адресации для строк на примере следующей задачи: найти число слов в строке при условии, что слова отделены друг от друга ровно одним символом пробела, и пробелов нет ни в начале строки, ни в конце. Задача сводится к задаче поиска всех вхождений символа.
// Прямая адресация
|
// Косвенная адресация
|
int i = 0;
int Cou=0;
do
{
if (Str[i] == ' ') Cou++;
i++;
} while (Str[i]!='\0');
printf ("Слов : %d", ++ Cou);
|
char *pts = Str;
Cou=0;
do
{
if (*pts == ' ') Cou++;
pts ++;
}while (*pts != '\0');
printf ("Слов : %d", ++ Cou);
|
Двумерные массивы строк весьма похожи на обычные двумерные массивы, каждая строка которого, это строка в смысле Си++. Согласно синтаксису Си++, все строки могут быть разной длины. С другой стороны, типичные задачи обработки текстовой информации, это обработка слов или предложений, каждое из которых должно быть разной длины. Именно потому для обработки строк целесообразно использование указателей, а не массивов с фиксированными размерами.
В примере описан и проинициализирован одномерный массив Rainbow[ ] указателей типа char*, каждый элемент которого – строка. Для вывода строк используется функция puts(), но и функция printf() со спецификатором преобразования %s допускает использование указателя на строку в качестве параметра. В выходной поток выводится не значение указателя Rainbow[i], а содержимое адресуемой им строки.
#include <stdio.h>
void main (void)
{ // Объявлен массив строк. Каждое слово – название цвета радуги.
// Память выделена при инициализации.
char * Rainbow[ ] = {"Красный", "Оранжевый", "Желтый", "Зеленый",
"Голубой", "Синий", "Фиолетовый"};
int i, len ;
len = sizeof (Rainbow) / sizeof (Rainbow [0]) ; // Определена длина массива строк.
for (i == 0;i < len; i++)
puts (Rainbow [i]) ;
}
В результате выполнения программы мы увидим на экране названия цветов радуги:
Красный
Оранжевый
И так далее.
1.3.5.5. Строки и функции
Особенность передачи строк в функции (параметром функции является строка)
Когда строка является параметром функции, то передача строки в функцию выполняется так же, как и для массивов. Строка в качестве формального параметра в заголовке функции описывается как char Имя_строки[] или char * Имя_строки. Строка в качестве фактического параметра в вызывающей программе может быть описана как одномерный массив типа char: char Имя[Константа], или как указатель типа char *: char * Имя = new char [80]. Всегда через параметр в функцию передается адрес начала символьного массива, содержащего строку. И функция, и вызывающая программа имеют одинаковые права на изменение содержимого строки.
Отдельно передавать длину строки нет необходимости, так как, во-первых, строка имеет признак конца '\0', который позволяет управлять циклами перебора символов строки, не выходя за ее пределы, а во-вторых, ее длину всегда можно определить, используя функцию strlen библиотеки <string.h>, подробнее о которой далее по тексту.
Рассмотрим пример функции преобразования строки, которая не изменяет размер строки, а только инвертирует ее содержимое.
void invert (char Str[ ])
{
char Buf;
int i, j, m;
for (m = 0; Str [m] != '\0'; m++); // Переменная m найдет положение '\0'.
for (i = 0, j = m – l; i < j; i ++, j – –)
{
Buf = Str[i];
Str[i] = Str[j];
Str[j] = Buf;
}
}
Функция не возвращает значения, так как она изменит содержимое строки, при этом символ '\0'остается на своем месте в конце строки. Пример использования функции invert():
void main (void)
{
char S[] = "0123456789";
// Вызов функции:
invert(S);
puts (S);
}
Результат выполнения программы:
9876543210
Функция, возвращающая указатель на строку
Пример функций, у которых тип возвращаемого значения отличен от базовых типов, уже был приведен для массивов. Для строк этот механизм используется очень часто, всегда, когда функция порождает новую строку. Следовательно, функция будет формировать новую строку, и должна ее вернуть. Может быть задействован механизм возвращения через параметры, или через указатель. В втором случае тип функции (возвращаемого ею значения) char* . Новая строка формируется в теле функции, для нее может быть выделена память, если она динамическая, и по оператору return Имя_указателя значение адреса строки возвращается в вызывающую программу.
Приведем пример программы, которая находит в строке слово наибольшей длины, и возвращает указатель на сформированный символьный массив.
char * Found_word (char *Str)
{
char *pts; // Синоним исходной строки Str.
char *ptw_beg; // Адрес первого символа очередного слова.
char *ptw_end; // Адрес последнего символа очередного слова.
char *ptw_max; // Адрес первого символа длинного слова.
int w_len = 0; // Длина очередного слова.
int w_len_max = 0; // Длина наибольшего слова.
pts = Str;
ptw_beg = pts;
do
{
if( (*pts = = ' ') || (*pts = = '\0') ) // Найдено очередное слово.
{
ptw_end = pts;
w_len = ptw_end – ptw_beg;
if( w_len > w_len_max ) // Если оно длиннее, запоминаем.
{
w_len_max = w_len;
ptw_max = ptw_beg;
};
while ( * pts = = ' ') // Пропуск серии пробелов между словами.
++pts;
ptw_beg = pts;
}
} while ( * pts++ );
// Выделение памяти для новой строки
char *Word = new char [w_len_max+1];
char *pW = Word;
// Копирование посимвольно.
for (pts = ptw_max; pts < ptw_max+w_len_max; pts++)
*pW++ = * pts;
*pW = '\0';
return Word;
}
// Пример использования функции.void main(void)
{
char Str_in[100];
puts ("Введите строку\n");
gets (Str_in);
char *Word_max;
Word_max = Found_word (Str_in);
puts(Word_max);
}
1.3.5.6. Библиотеки функций для работы с символами и со строками текста
В состав стандартных библиотек входят библиотеки работы с символами и со строками текста, в которых содержатся многие полезные функции обработки текстовых данных.
Библиотека <ctype.h> содержит функции и макросы проверки и преобразования символов. Покажем прототипы некоторых из них.
int isalpha (int c) Возвращает отличное от 0 значение, если с – латинская буква.
int isascii (int c) Возвращает отличное от 0 значение, если с – символ кода ASCII (от 0 до 127).
int iscntrl (int c) Возвращает отличное от 0 значение, если с – управляющий символ с кодами 0х00 – 0х01F.
int isdigit (int c) Возвращает отличное от 0 значение, если с – символ цифры от 0 до 9.
int isspace (int c) Возвращает отличное от 0 значение, если с – обобщенный пробельный символ.
int toascii (int c) Преобразует целое число в символ кода ASCII, обнуляя все биты, кроме младших, результат от 0 до 127.
int tolower (int v) Преобразует код буквы латинского алфавита к нижнему регистру, прочие коды не изменяются.
int toupper (int c) Преобразует код буквы латинского алфавита к вержнему регистру, прочие коды не изменяются.
Библиотека <stdlib.h> содержит полезные функции преобразования строк . Покажем прототипы некоторых из них.
double atof (const char *Str) Преобразует строку Str в вещественное число.
int atoi (const char *Str) Преобразует строку Str в целое число.
long atol (const char *Str) Преобразует строку Str в длинное целое число.
char * itoa (int Val, char *Str, int Base) Преобразует целое число Val в строку Str. Base – основание системы счисления.
char *ecvt(doubleVal, int ndig, int *dec, int *sign) Преобразует вещественное число Val в строку.
Библиотека <string.h> содержит полезные функции преобразования строк. Покажем прототипы некоторых из них.
Функции преобразования строк.
char *strcat (char *Str1, const char *Str2) Присоединяет (приписывает) строку Str2 к строке Str1 (конкатенация строк).
char *strchr (const char *Str, int C) Ищет в строке Str первое вхождение символа С. Возвращает указатель на первое вхождение символа, или NULL, если его нет.
int strcmp (const char *Str1, const char *Str2) Сравнивает строки Str2 и Str2. Результат отрицателен, если Str1<Str2, равен 0, если Str1==Str2, и положителен, если Str1>Str2.
char *strcpy (char *Str1, char *Str2) Копирует строку Str2 в строку Str1. Возвращает указатель на копию строки.
unsigned strlen (const char *Str) Возвращает целочисленную длину строки Str1, включая '\0'.
char *strlwr (char *Str) Преобразует буквы латинского алфавита из верхнего регистра к нижнему.
char *strupr (char *Str) Преобразует буквы латинского алфавита из нижнего регистра к верхнему.
char *strncat (char *Str1, const char *Str2, int Cou) Приписывает Cou символов строки Str2 к строке Str1. Первый символ пишется на место нуль-символа.
int strncmp (const char *Str1, const char *Str2, int Cou) Сравнивает части строк Str1 и Str2 в количестве Cou. Результат отрицателен, если Str1<Str2, равен 0, если Str1==Str2, и положителен, если Str1>Str2.
char * strncpy (char *Str1, const char *Str2, int Cou) Копирует Cou символов строки Str2 в строку Str1.
char *strpbrk (const char *Str1, const char *Str2) Ищет в строке Str1 первое появление любого из символов, входящих в строку Str2. Возвращает указатель на символ, а если его нет, то NULL.
char *strrchr (const char *Str, int C) Отыскивает в строке Str последнее вхождение символа С. Возвращает указатель на символ, а если его нет, то NULL.
char *strset (char *Str, int C) Заполняет строку Str заданным символом С.
char *strstr (const char *Str1, const char *Str2) Ищет в строке Str1 подстроку Str2. Возвращает указатель на тот элемент в строке Str1, с которого начинается подстрока Str2, или NULL, если поиск неудачен.
Пример.
char Str1[25];
char *Str2 = " ", *Str3 = "C++", *Str4 = "Borland";
strcpy (Str1, Str4); // Str1 = "Borland";
strcat (Str1, Str2); // Str1 = "Borland ";
strcat (Str1, str3); // Str1 = "Borland C++";
…
char *ptr, C = 'r';
strcpy (Str1, "This is a string");
ptr = strchr (Str1, C);
if (ptr)
printf("Символ %c в позиции %d\n", C, ptr – Str1); // В 12-й позиции.
else
printf("Символа нет\n");
…
char *Str5 = "Aaa", *Str6 = "aAa";
int Res;
Res = strcmp(Str5, str6);
if (Res > 0)
printf("Больше\n");
else
printf("Меньше\n");
…
char *Str = "Borland International";
printf("Длина строки %d\n", strlen (Str));
…
char *Str7 = "на Си и Си++";
strcpy (Str1, "Введение в программирование ");
strncat (Str1, Str7, 6); // Str1 = "Введение в программирование на Си ".
В качестве примера рассмотрим использование функций стандартной библиотеки <string.h> в задаче сортировки в алфавитном порядке строк текста. Нужно сравнить размер функции сортировки с ранее описанными функциями копирования строк.
#include <string.h>
#define LEN 80 // Наибольшая длина строки.
#define SIZE 20 // Наибольшее число строк в массиве.
// Входные параметры функции сортировки строк, это двумерный массив строк
// текста и их количество. Алгоритм – пузырьковый метод.
void SortSt (char *St [LEN], int num)
{
int i;
int flag;
char Buf [LEN]; // Буферная переменная для перестановки.
flag = 1;
while (flag != 0)
{
for (i = 0, flag = 0; i < num – 1; i ++)
{
if (strcmp ( St[i], St[i+1]) > 0)
{
strcpy (Buf, St[i]);
strcpy (St[i], St[i+1]);
strcpy (St[i+1], Buf);
flag = 1;
}
}
}
}
// Пример использования функции.void main(void)
{
char S [SIZE][LEN]; // Массив вводимых строк.
char *ps [SIZE]; // Массив указателей на строки массива.
int i, num; // Количество вводимых строк.
printf ("Введите строки до пустой строки.\n");
num = 0;
while(( gets (S[num]) != NULL ) && (num <= SIZE) && strcmp (S[num],"") != 0)
// strcmp позволит сравнить значения символов,
// а не указатели: S – пустая строка.
{
ps [num] = S[num]; // Указатель на взятую функцией строку.
num++;
}
printf ("Вывод перед сортировкой:\n");
for (i = 0; i < num; i++)
puts (S[i]); // Введенный массив строк.
printf("\n");
// Передача в функцию сортировки.
SortSt (ps, num); // Передается весь массив.
printf ("Вывод после сортировки:\n");
for (i = 0; i < num; i++)
puts (S[i]); // Упорядоченный массив строк.
printf("\n");
// Указатель на сортированные строки.
for (i = 0; i < num; i ++)
puts(ps[i]);
printf("\n");
}
1.3.6. Структуры и объединения.
Определение. Структура, это множество именованных данных, объединенное в единое целое.
Как и массив, структура предназначена для логического объединения данных в более крупные единицы, чем данные простых типов. Массив всегда состоит из однотипных элементов, и все они имеют общее имя. Элементы (компоненты, поля) структуры могут быть разных типов, и все имеют различные имена. Например, для описания такого данного как товар, хранящийся на складе, необходимо знать его основные характеристики:
- название товара, строка;
- оптовая цена, вещественное данное;
- торговая наценка в процентах, целая или вещественная величина;
- размер партии товара, целое число или вещественное;
- дата поступления партии товара, строка.
1.3.6.1. Описание структурного типа
Описание структуры конструирует новый тип данного, объединяющий в себе разнотипные разноименные компоненты, имеет синтаксис:
struct имя_структурного_типа
{
определения элементов;
}; // «;» завершает описание.
Здесь struct – ключевое слово, обозначающее структурный тип;
имя_структурного_типа – произвольное имя, которое идентифицирует сконструированный тип;
определения элементов (компонентов, полей) – описания объектов, входящих в состав структуры, каждый из которых есть прототип одного из данных, входящих в состав вводимого структурного типа.
Описание структурного типа заканчивается точкой с запятой.
Например, чтобы ввести структуру «товар на складе», следует выбрать имена и типы данных для перечисленных выше характеристик товара.
struct Goods
{
char *name; // Наименование.
float price; // Оптовая цена.
float percent; // Торговая наценка в %.
int vol; // 06ъем партии товара.
char date [9]; // Дата поставки партии товара.
};
Имя структурного типа Goods. Это имя является именем типа данного, который в общем виде описывает объект «товар на складе». Наименование товара будет связано с указателем типа char* и именем name, оптовая цена единицы товара будет значением типа float с названием price, и так далее.
1.3.6.2. Объявление объекта структурного типа
Введенное описание структурного типа именует абстрактный тип данного, сконструированного программистом, и далее может использоваться для объявления переменных этого типа, так же как для базовых типов используются спецификаторы, например, double или int.
Синтаксис такого объявления:
struct имя_структурного_типа имя_переменной;// Переменная.
struct имя_структурного_типа * имя_переменной; // Указатель.
Так вводятся фактические объекты указанного типа или указатели на них.
Например,
struct Goods food;
struct Goods *p_food;
1.3.6.3. Использование typedef для конструирования структур
Служебное слово typedef позволяет ввести собственное обозначение для любого определения типа. Для структурного типа синтаксис таков:
typedef struct
{
определения элементов;
} обозначение_структурного_типа;
Пример для того же объекта:
typedef struct
{
char *name; // Наименование.
float price; // Оптовая цена.
float percent; // Торговая наценка в %.
int vol; // 06ъем партии товара.
char date [9]; // Дата поставки партии товара.
} Goods ;
Это описание вводит структурный тип struct {определения элементов}, и присваивает ему обозначение_структурного_типа. Обозначение имеет тот же смысл, что и ранее (название, имя), и так же используется для объявления объектов структурного типа, например:
Goods food1, food2, food3;
Объявлены три структуры: food1, food2, food3, каждая из которых представляет реальный объект.
Инструкция typedef назначает имя структурному типу, который может в то же время иметь второе имя, вводимое стандартным образом после служебного слова struct. Это имя может быть синонимом первому.
typedef struct Merchandise
{
char *name; // Наименование.
float price; // Оптовая цена.
float percent; // Торговая наценка в %.
int vol; // 06ъем партии товара.
char date [9]; // Дата поставки партии товара.
} Goods;
Здесь Goods – обозначение структурного типа, введенное с помощью typedef, а имя Merchandise введено для того же типа стандартным способом. После такого определения структуры, объекты этого типа могут вводиться как с помощью названия Goods, так и с помощью обозначения того же типа struct Merchandise.
1.3.6.4. Объявление данных совместно с конструированием типа
Изложенная последовательность определения структур (определить структурный тип, затем ввести переменные этого типа) является самой логичной из всех, но в языке Си++ имеются еще две схемы определения данных типа структура.
Во-первых, структуры можно ввести в употребление одновременно с определением структурного типа:
struct имя_структурного_типа
{
определения_элементов;
}
список_структур; // Это имена переменных структурного типа.
Пример одновременного определения структурного типа и объектов этого типа:
struct Student
{
char name [15]; // Имя.
char surname [20]; // Фамилия.
int group; // Группа.
} student_1, student_2, student_3;
Здесь определен тип с именем Student, и три объекта этого типа student_l, student_2, student_3, которые являются полноправными объектами. В каждую из этих трех структур входят элементы, позволяющие представить имя (name), фамилию (surname), группу (group).
После приведенного определения в той же программе можно определять любое количество структур, используя структурный тип student:
struct Student leader, freshman;
Во-вторых, можно определять структуры, не вводя названия типа. Безымянный структурный тип обычно используется в программе для одно кратного определения структур:
struct
{
определения элементов;
} список_структур; // Это имена переменных структурного типа.
Пример безымянного структурного типа, описывающего конфигурацию персонального компьютера:
struct
{
char processor [10]; // Тип процессора.
int frequency; // Тактовая частота в МГц.
int memory; // Объем основной памяти в Мбт.
int disk; // Объем жесткого диска в Гбт.
} IBM__586, IBM_486, Compaq;
Введены три объекта с именами IBM_586, IВМ_486, Compaq. В каждую из них входят элементы, в которые можно занести сведения о характеристиках конкретных компьютеров. Структурный тип «компьютер» не именован, поэтому, если в программе потребуется определять другие структуры с таким же составом элементов, то придется полностью повторить приведенное выше определение.
1.3.6.5. Выделение памяти и инициализация структур
Определения элементов, входящих в данное структурного типа, похожи на объавления данных соответствующих типов, но сам структурный тип не является объектом. Следовательно, при конструировании (описании) структурного типа его компонентам не выделяется память, и их нельзя инициализировать.
Для работы с объектами структурного типа следует объявлять переменные этого типа, например:
struct Merchandise dress, footwear, toy;
Если имя структурного типа введено с помощью typedef, то определение не содержит слова struct, например:
Goods *food, *drink;
Описание структурного типа не связано с выделением памяти. Только при объявлении объекта типа структуры, ему выделяется память в таком количестве, чтобы могли разместиться данные всех элементов структуры. Покажем на рис.1 1.2.6.5.1 условную схему распределения памяти для одного из объектов структуры типа Goods, описанной ранее.
Названия элементов
|
name
|
price
|
percent
|
vol
|
date
|
|
Типы
|
char *
|
float
|
float
|
vol
|
char [9]
|
|
Объем памяти
|
2 байта
|
4 байта
|
4 байта
|
2 байта
|
9 байт
|
|
Рис. 1.2.6.5.1. Размещение в памяти объекта структурного типа
На рисунке элементы структуры размещены подряд, без пропусков между ними, но не обязательно размещение элементов структур будет непрерывным. Возможно появление неиспользованных участков памяти вследствие требования выравнивания данных по границам участков адресного пространства. Эти требования зависят от реализации, от аппаратных возможностей системы и иногда от режимов работы компилятора. Вследствие этого может изменяется общий объем памяти, выделяемый для структуры. Реальный размер памяти в байтах, выделяемый для структуры, можно определить с помощью операции sizeof:
sizeof (имя_структурного типа)
или
sizeof (имя_объекта_структурного_типа).
Инициализация структур похожа на инициализацию массивов. При объявлении объекта структуры в фигурных скобках после имени и знака присваивания размещается список начальных значений элементов, например:
struct Goods coat =
{
" Черная шляпа", 1000.00, 15.0, 100, "12.01.04"
};
При этом элементы объекта coat получают соответствующие начальные значения.
1.3.6.6. Операции над структурами. Доступ к элементам структур
Для структур стандарт языка Си++ разрешает присваивание, если операнды одного типа. (Для массива в целом операция присваивания не допускается).
struct Goods dress;
Допустимо следующее присваивание:
dress = coat;
Никакие операции сравнения для структур не определены. Сравнивать структуры следует только поэлементно.
Для доступа к элементам структур используется операция разыменования «точка» или уточненные имена полей структуры. Синтаксис:
имя_структуры.имя_элемента
Здесь имя_структуры, это имя объекта структурного типа, а имя_элемента, это имя одного из элементов в соответствии с определением структурного типа.
Например, в примере с инициализацией структуры типа struct goods:
coat.name – указатель типа char* на строку "пиджак черный",
coat.price – переменная типа float со значением 4000.00;
coat.percent – переменная типа float со значением 15.0;
coat.vol – переменная типа int со значением 500;
coat.date – массив типа char [9], содержащий "12.01.04".
Перед точкой стоит имя конкретной структуры, для которой при ее объявлении выделена память.
Операция «точка» называется операцией прямого доступа к элементу структуры. Имеет два операнда: имя объекта и имя элемента структуры. Тип возвращаемого значения, это тип элемента структуры. Имеет самый высокий ранг наряду со скобками и операцией «стрелка» для доступа к элементам структуры через указатель. Уточненные имена элементов структур обладают всеми правами объектов соответствующих типов, их можно использовать в выражениях.
Например:
Функция scanf изменит торговую наценку (элемент coat.price), в функции printf выражение вычисляет розничную цену на товар (Черная шляпа):
printf ("\n Введите торговую наценку;");
scanf ("%f", &coat.percent);
printf ("Ценa товара: %f руб.\n", coat.price*(1.0+coat.percent/100.)) ;
Адрес элемента percent структуры coat используется как параметр функции scanf, для чего операция & применяется к имени coat.percent.
1.3.6.7. Структуры, массивы и указатели
Рассматривая соотношение между этими типами, можно выделить возможные варианты.
1.2.6.7.1. Массивы и структуры в качестве элементов структур
Массив, как и любые другие данные, может быть элементом структуры, тогда для обращения к каждому элементу такого массива требуется операция разыменования элемента массива. Символьные массивы как элементы структуры уже были использованы в примере, например, название товара, или дата поставки.
Пример.
Получим дату поставки партии товара, это coat.date = "12.01.04". Чтобы выделит из даты, например, год поставки, следует выделить 6-й и 7-й по индексу элементы массива.
char To_year[3];
…
To_year[0] = coat.date[6];
To_year[1] = coat.date[7];
To_year[2] = ′\0′;
Как видим, операция разыменования применяется к элементу массива, как обычно. Для именования массива используется его уточненное имя.
Элементом структуры может быть другая структура, это называется вложение структур. Например, структурный тип для представления сведений о студенте, дополнительно к тем элементам, которые уже есть (имя, фамилия, группа), может содержать данное «дата рождения». Это может быть структура с элементами «число», «месяц», «год».
В тексте описания структурных типов должны быть размещены в такой последовательности, чтобы использованный в описании структурный тип был уже определен ранее:
struct Date // Тип «дата».
{
int day; // Число
int month; // Месяц
int year; // Год
};
При обращении к такому элементу операция «точка» используется дважды, так как имя поля нуждается в уточнении дважды.
void main (void)
{// Конкретная структура:
struct Student stud_1 =
{
"Павел", "Колесников", 145, 22, 04, 1988
};
printf ("\n Введите группу: ") ;
scanf ("%d", &stud_1.group);
printf ("Сведения о студенте:");
printf ("\n Фамилия: %s", stud_1.surname) ;
printf ("\n Имя:%s", stud_1.name);
printf ("\n Группа: %d\n", stud_1.group) ;
printf ("\n Дата рождения: %d.%d.%d", \
stud_1.date.day, stud_1.date.month, stud_1.date.year);
Объект stud_1 типа struct Student получает значение элементов при инициализации. Затем с помощью ввода изменяется элемент stud.group, и содержимое структуры выводится на экран. Для доступа к элементам вложенных структур используются дважды уточненные имена.
1.2.6.7.2. Массивы структур.
Массивы структур вводятся в употребление с помощью описания так же, как и массивы других типов данных с указанием имени структурного типа, например:
struct Goods List [MAX];
Определен массив List, состоящий из MAX элементов, каждый из которых является структурой типа Goods. Имя List, это имя массива, элементами которого являются структуры. List[0], List[l] и так далее, все они структуры типа Goods.
Для доступа к полям структур, входящих в массив структур, используются уточненные имена с индексированием первого имени (имени массива). Индекс записывается непосредственно после имени массива структур. Тем самым из массива выделяется нужная структура, а уже с помощью точки и последующего имени идентифицируется соответствующий компонент структуры, например:
List[0].price – элемент с именем price структуры типа Goods, входящей в качестве первого элемента (с нулевым индексом) в массив структур List[].
При размещении в памяти массива структур элементы массива размещаются подряд в порядке возрастания индекса. Каждый элемент массива занимает столько места, сколько необходимо для размещения структуры.
Как и массивы других типов, массив структур при определении может быть инициализирован. Инициализатор массива структур должен содержать в фигурных скобках список начальных значений структур массива. В свою очередь, каждое начальное значение для структуры – это список значений ее компонентов (также в фигурных скобках).
1.2.6.7.3. Указатели на структуры
Указатели на структуры вводятся в употребление, как и указатели на данные других типов.
Например:
struct Goods *p_goods;
Для безымянных структурных типов:
struct
{ // Безымянный структурный тип
char processor [10]; // Тип процессора
int frequency; // Частота в МГц
int memory; // Память в МБт.
int disk; // Емкость диска в МБт.
} *point_IBM, *point_2 ;
Если название структурного типа введено с помощью typedef, то объявление указателя выглядит как обычное объявление указателя.
При определении указателя на структуру он может быть инициализирован. Наиболее корректно в качестве инициализирующего значения применять адрес структурного объекта того же типа, что и тип определяемого указателя:
struct Particle
{
double mass;
float coord [3];
} dot [3], point, *p_point;
// Инициализация указателей:
struct Particle *p_d = &dot [0];
Значение указателя на структуру может быть определено и с помощью обычного присваивания:
p_point = & point;
Если используется указатель на структуру, адресующий конкретный объект, то доступ к ее элементам обеспечивается с использованием операции косвенного разыменования «стрелка», синтаксис которой:
указатель_на_структуру –> имя_элемента
Имеет два операнда: имя структуры, которую адресует левый операнд, и имя элемента структуры. Тип результата операции «стрелка» совпадает с типом правого операнда, то есть того элемента структуры, на который она нацелена. Имеет самый высший ранг, наряду со скобками и операцией «точка», обеспечивает доступ к элементу структуры через адресующий ее указатель того же структурного типа.
Примеры:
p_point –> mass эквивалентно (*p_point).mass;
p_point –> coord[0] эквивалентно (*p_point).coord[0].
Изменить значения элементов структуры можно, используя присваивание:
p_point –> mass = 18.4;
for (int i = 0; i < З; i++)
p_point –> coord[i] = 0.1*1;
1.2.6.7.4. Указатели на структуры как компоненты структур
Элементом структуры может быть указатель на структуру, тип которой уже определен. Обычна конструкция:
struct Array
{
int M;
float * d;
};
struct Matrix
{
int N;
struct *Array[];
};
Здесь указатель на ранее определенную структуру Array является элементом структуры типа struct Matrix.
В то же время элементом структуры может быть указатель на структуру того же типа, что и определяемый структурный тип:
struct Element
{
// Структурный тип «химический элемент»
int number; // Порядковый номер/
float mass; // Атомный вес.
char *name[16]; // Название элемента;
struct Element * next;
};
В структурном типе «химический элемент» элемент next, это указатель на структуру того же типа. С его помощью можно формировать динамические списки, например, связать все элементы одной группы периодической таблицы Д. И. Менделеева.
1.3.6.8. Структуры и функции
Возможны всего два варианта: структура может быть передана функции как параметр, или структура может быть возвращаемым функцией значением. И в том, и в другом случае могут использоваться указатели на объекты структурных типов. Рассмотрим возможные варианты. Пусть есть структура:
struct Person
{
char * name;
int age;
};
Покажем, обязательные синтаксические конструкции в прототипах функций, которые работают со структурами:
1. Параметр функции – объект структурного типа:
void F1 (struct Person Имя_объекта);
2. Параметр функции – указатель на объект структурного типа:
void F2 (struct Person * Имя_объекта);
3. Функция возвращает структуру:
struct Person F3 (Список_параметров);
4. Функция возвращает указатель на структуру:
struct Person * F4 (Список_параметров) ;
Механизмы передачи параметров и возвращения значения функцией подробно рассмотрены в первой главе.
1.3.6.9. Объединения
Определение. Объединение – структура, все элементы которой имеют нулевое смещение от ее начала.
Объединения определяются с помощью служебного слова union. При размещении в памяти разные элементы объединения занимают один и тот же участок. Тем самым объединения обеспечивают возможность доступа к одному и тому же участку памяти с помощью объектов разного типа. Необходимость в такой возможности возникает, например, при выделении из внутреннего представления целого числа его отдельных байтов. Введем такое объединение:
union
{
char hh[2] ;
int ii;
} CC;
union – служебное слово, вводящее объединяющий тип данных;
СС – имя объединения;
символьный массив hh и целая переменная ii – поля объединения.
Схема размещения объединения CC в памяти приведена на рис. 1.2.6.9.1. Для объединения выделяется такой объем памяти, чтобы разместить наибольший из элементов объединения. Размещение всех элементов – от начала одного и того же участка памяти.
Выделенная память
|
1 байт
|
1 байт
|
|
Размещение символьного массива
|
hh[0]
1 байт
|
hh[1]
1 байт
|
|
Размещение целой переменной
|
ii
2 байта
|
|
Рис 1.2.6.9.1. Схема размещения объединения в памяти
Для обращения к элементу объединения используются те же конструкции, что и для обращения к элементу структуры:
имя_о6ъединения.имя_элемента
указатель на объединение –> имя_элемента
Смысловое отличие объединения oт структуры состоит в том, что записать информацию в объединение можно с помощью одного из его элементов, а выбрать данные из того же участка памяти можно с помощью другого элемента того же объединения. Например, оператор
CC.ii= 16;
записывает значение 15 в объединение, а с помощью конструкций CC.hh[0] и CC.hh[l] можно получить отдельные байты внутреннего представления целого числа 15.
1.3.7. Ввод-вывод данных в Си++
В языке Си++ нет средств ввода-вывода. Этим обеспечивается аппаратная независимость языка. Ввод-вывод реализуется посредством библиотек стандартных функций языка Си++ (стандарт ANSI C). Библиотека stdio.h содержит средства ввода-вывода (обмена с устройствами), в том числе с файлами на диске. С точки зрения языка нет разницы, с устройством или файлом происходит обмен.
Существует три уровня ввода-вывода:
1. Верхнего уровня (потоковый).
2. Записями (низкоуровневый).
3. Для консоли и портов. Это системно зависимый обмен.
1.3.7.1. Потоковый ввод - вывод
Определение. Поток – последовательность байтов (символов), не зависящая от устройства обмена данными (файл, консоль, принтер и пр.).
Обмен с устройством выполняется через буфер потока. Буфер, это фрагмент памяти, который выполняет роль промежуточной ступени при передаче информации с (на) внешнее устройство. Увеличивает скорость обмена, так как реальная передача данных выполняется при заполнении буфера. При обмене с диском размер буфера определен способом хранения данных на диске, и равен размеру кластера (512 или 1024 байта). Одна операция обращения передает блок данных из (в) буфера обмена.
При обмене с потоком файл (поток) трактуется как сплошной поток байт, текущих через устройство. Позиция чтения (записи) называется указателем потока (не путать с типом данных «указатель»), или индикатором потока. Указатель потока (позиция чтения-записи) показывает на очередную порцию данных, которая может быть прочитана (записана) при выполнении следующей операции ввода-вывода (обмена).
При открытии файла для чтения (записи) указатель потока показывает на первый байт потока, при открытии для добавления на тот, что стоит за последним байтом.
При чтении позиция указателя установлена на очередную порцию считываемых данных, операция чтения возвращает текущее значение указателя (значение прочитанного данного).
Изменить позицию указателя могут функции позиционирования в потоке или закрытие файла.
Примерная картина потока представлена на рисунке 1.2.7.1.1.
12.5
|
|
W
|
|
Си++
|
|
|
99.0
|
|
2004г.
|
|
|
5
|
feof
|
|
|
|
|
Рисунок 1.2.7.1.1. Поток – последовательность байтов, текущая через устройство
Задача программиста – правильная интерпретация байт потока. Программа должна выделить из сплошной последовательности символов объекты, обладающие типом и участвующие в обмене.
По направлению обмена типы потоков можно разделить на:
1) входной,
2) выходной,
3) двунаправленный.
Все типы последовательные, то есть в любой момент времени для потока определены позиции чтения (записи), а при выполнении операции обмена происходит смещение на длину переданной порции данных.
Поток можно условно присоединить к устройству. В зависимости от устройства потоки можно разделить на стандартные, консольные, строковые, файловые.
Консольные потоки (для DOS) организуют ввод с клавиатуры и управление экраном. Стандартные потоки для консоли:
Ввода stdin - клавиатура
Вывода stdout - экран
Сообщений об ошибках (обмена) srderr - экран
Можно перенаправить потоки в файлы на диске.
1.3.7.2. Файлы
Определение. Файл – именованная область внешней памяти, в которой содержится некоторая информация. Например, тексты программ хранятся в виде текстовых файлов. Файлы также могут хранить данные, а программы могут обращаться к файлам данных для чтения информации или записи.
Файлы удобно использовать, когда программа обрабатывает большой объем информации, или когда данные должны храниться на внешних устройствах, или когда данные могут быть исходными для нескольких программ обработки.
Программы, которые работают с данными, хранящимися в файле, чаще всего, временно размещают их в оперативной памяти. Этот прием позволяет увеличить скорость обработки данных и снизить сложность программ. Данные из файлов, будучи прочитаны программой, становятся значениями объектов программы, например, массивами, матрицами и пр.
1.3.7.3. Типы файлов
По механизму хранения данных и обращения к ним файлы разделяются на файлы последовательного доступа и файлы прямого доступа.
Файлами последовательного доступа являются текстовые файлы. Такие файлы подготавливаются в текстовом редакторе и хранят данные в символьном представлении. Их можно легко просматривать и редактировать. Число символов для представления каждого данного различно. Как правило, Си++ распознает лексему, и интерпретирует ее как одно данное. Для отделения лексем друг от друга используются обобщенные пробельные символы. Чтобы получить какое-нибудь данное потока, нужно последовательно прочитать все предыдущие данные. В текстовом файле могут храниться данные различных типов Си++.
Файл прямого доступа, это двоичный файл. Хранит данные одного типа, не обязательно базового. Каждое данное хранится во внутреннем представлении, размер определен типом данного. Чтобы получить какое-нибудь данное, можно переместить указатель потока непосредственно на это данное, и выполнить операцию обмена.
1.3.7.4. Использование файлов
Для того чтобы программа могла использовать файловый ввод-вывод, необходимо выполнить следующие действия.
1. Открыть (закрыть) поток. Для этого нужно объявить указатель на поток и связать его с физически существующим файлом.
2. Передать данные в файл (из файла). Для этого используются функции ввода- вывода.
Кроме того, можно:
3. Отсекать ошибки обмена данными. Для этого существуют функции обработки ошибок.
4. Управлять буфером обмена. Для этого используются функции буферизации потока (размер).
5. Указать на позицию в потоке. Для этого используются функции перемещения указателя файла.
1.2.7.4.1. Объявление файловой переменной
Для объявления логического имени файла используется тип FILE, описание которого находится в библиотеке stdio.h. Это структура данных, которая хранит данные о файле, такие как размер буфера, позиция в потоке и прочие.
Объявление файла:
FILE *имя_файла;// имя_файла, это имя переменной, связанной
// с файлом, указатель на стандартную
// структуру данных FILE.
Это объявление аналогично объявлению обычных переменных, так же как:
int *a;
FILE *имя;
Например:
FILE *in, *out; // Файл in для ввода, out для вывода.
FILE *my_file; *my_other_file;
Этот указатель можно назвать логическим именем файла, под которым файл будет известен программе, и будет использоваться во всех последующих операциях.
1.2.7.4.2. Открытие файла
Смысл этой операции в том, чтобы связать логическое имя файла с файлом, физически существующим на диске. Логическому имени файла присваивается значение, возвращаемое функцией fopen. В случае ошибки открытия файла fopen возвращает NULL. Функция имеет два параметра, оба строкового типа.
Формат обращения к функции:
имя_файла = fopen("имя_физического_файла", "режим_открытия_файла");
например:
in = fopen("input.txt", "r");
out = fopen("output.txt", "wb");
Имя физического файла, это строка, значение которой определяет имя файла, возможно, с указанием полного пути к нему. Режим открытия, это строка, определяющая режим доступа к потоку и тип файла, содержит один или два символа. Первый символ определяет тип операций обмена.
r – для чтения. Файл должен существовать на диске. Логическое имя файла связывается с физическим именем. Позиция чтения устанавливается перед первым байтом файла.
w – для записи. Если файл существует на диске, то он будет открыт для записи. Позиция записи устанавливается перед первым байтом файла. Если в файле была информация, она будет утеряна. Если файл не существует, он будет создан.
a – для добавления в конец. Если файл существовал на диске, он будет открыт для записи. Позиция записи устанавливается перед признаком конца файла. Если файла не было, он будет создан.
Признак + после типа файла, r+, w+, a+ расширяет возможности операций обмена, модифицируя тип файла. С таким типом файл открывается как для чтения, так и для записи.
r+ – существующий файл открывается как для чтения, так и для записи в любом месте файла. Запись в коней недопустима, так как недопустимо увеличение размера файла.
w+ – новый файл открывается для записи и последующих изменений. Если файл существует, прежнее содержимое стирается. Последующие операции записи и чтения допустимы в любом месте файла, в том числе в конец, то есть размер файла может увеличиваться.
a+ – файл открывается или создается и становится доступным для изменений, то есть для записи и чтения в любом месте файла. В отличие от w+ при открытии существующего файла его содержимое не уничтожается, в отличие от r+ можно добавить запись в конец файла.
Второй символ определяет тип файла, следовательно, тип хранения данных.
t – текстовый файл. Действует по умолчанию.
b – двоичный файл.
Физический файл может находиться где угодно. Имя файла можно указывать с полным путем. Если имя краткое, файл будет отыскиваться только в текущем каталоге.
fopen("c:\\work\\f.txt","wt");
Имя физического файла может изменяться, тогда параметр функции fopen должен быть переменной строкового типа.
char *name_of_file = new char [50];
...
puts ("Введите имя файла\n");
gets (name_of_file);
fopen (name_of_file,"wt"); // Запишет в файл, который
// укажет пользователь при работе.
1.2.7.4.3. Ошибки открытия потока
Существуют некоторые обычные ошибки открытия файла, например, файл не найден, диск заполнен, недостаточно динамической памяти для выполнения операции и прочие. При любой ошибке открытия файла fopen возвращает NULL. Это используется для того, чтобы обрабатывать возможные ошибки ввода-вывода. В случае возникновения одной из штатных ошибок ввода-вывода, ее код errno распознает и обрабатывает функция perror.
FILE *my_file;
…
if ((my_file = fopen("f.txt","wt")) = = NULL) // Текстовый для записи.
{
perror ("Ошибка открытия файла"); // Если есть ошибка, ее код = errno.
exit(); // perror анализирует номер ошибки
} // и выводит поясняющий текст.
Прототип функции perror находится в stdio.h. Там же определена переменная int errno. Ею пользуются многие функции Си++, в том числе, функции ввода-вывода. fopen, обнаружив ошибку, заносит ее код в переменную errno. Функция perror выводит на экран текстовую строку (свой аргумент), затем двоеточие, пробел и сообщение об ошибке, содержимое и формат которого определены реализацией.
Если файл не найден, а ошибка не анализируется, поток ввода перенаправляется на стандартный поток ввода-вывода (консоль), с попыткой чтения из буфера обмена.
1.2.7.4.4. Закрытие файла
Закрытие файла, это высвобождение логического имени файла. Используется, чтобы отвязать логическое имя от физического файла. Логическое имя не перестает существовать, и может быть использовано повторно для других целей, например, для изменения режима работы с файлом.
Синтаксис:
fclose (имя_файла);
Пример:
fclose (in);
fclose (out);
При завершении работы программы все открытые в ней файлы будут закрыты, даже если операция закрытия не была выполнена программистом. Если файл закрывает программист, то все данные из буфера выводятся в файл перед его закрытием. Если же файл закрывается при завершении программы, то некоторые данные могут быть потеряны вследствие механизма буферизации обмена.
1.2.7.4.5. Конец файла
Признак конца файла (end-of-file) присутствует в конце файла всегда. Чтобы его распознать в потоке, используется макроопределение feof. С его помощью можно проверить, найден ли в потоке признак конца файла (end-of-file).
Объявление:
int feof (FILE *stream);
Как видим, тип возвращаемого значения int, следовательно, данная функция возвращает целое значение (точнее, логическое). feof проверяет данный поток на наличие end-of-file в текущем положении указателя потока. Возвращаемое значение отлично от нуля, если при выполнении последнего оператора ввода для потока найден end-of-file, и равно нулю, если end-of-file не обнаружен.
1.3.7.5. Чтение и запись для текстовых файлов
Все функции ввода-вывода, которые были рассмотрены ранее, работают по принципам потокового ввода-вывода, используя стандартные текстовые потоки stdin, stdout, srderr.
Для символов:
int getc () // Читает символ (int) c клавиатуры stdin.
int putc (int C) // Выводит символ C на экран stdout.
2. Для строк:
char *gets (char *S) // Читает строку S (char *) c клавиатуры stdin.
int puts (char *S) // Выводит строку S на экран stdout.
3. Для форматированных данных:
int scanf (const char* format,…)// Читает по форматному вводу из stdin.
int printf (const char* format,…)// Выполняет форматный вывод в stdout .
При работе с файлами для текстовых файлов используются другие функции библиотеки <stdio.h>, отличительным признаком которых является буква f в начале имени функции. Каждая из них имеет, по сравнению с вышеперечисленными функциями, дополнительный параметр, это логическое имя файла, для которого выполняется данная операция. Остальные механизмы остаются неизменными. Прототипы этих функций в библиотеке stdio.h описаны следующим образом.
1. Для символов:
int fgetc (FILE *stream) ;
Файл определен указателем stream. Функция возвращает очередной символ как тип int. Если чтение не может быть выполнено, возвращает end-of-file.
int fputc (int С, FILE *stream);
Функция записывает символ С в файл, определенный указателем stream. При ошибке возвращает end-of-file, в случае успешной операции – записанный символ.
Например, обращение для ввода и вывода символа:
char С;
С = fgetc (in); // Символ С получен из файла in.
fputc (out, С); //Символ С выведен в файл out.
2. Для строк:
char *fgets (char *S, int n, FILE *stream);
При вводе строки параметр n означает наибольшее количество символов, прочитываемых из файла, определенного указателем stream. Как правило, совпадает с длиной строки по ее описанию. Функция читает из файла символы до символа новой строки '\n', который переносится в строку S. Если такой символ отсутствует в строке, то будет введено ровно n – 1 символов. В конец строки S записывается нулевой символ '\0'. Функция возвращает указатель S при успешном завершении ввода, или NULL.
int fputs (const char *S, FILE *stream);
Функция fputs записывает строку S, в файл, определенный указателем stream. Символ конца строки '\0' не переносится в файл. Возвращает целое, равное числу записанных символов. При ошибке возвращает end-of-file.
Обращение к функциям строкового обмена самое естественное.
char S[80];
fgets (S, 80, in);
fputs (out, S);
3. Для форматированного ввода-вывода данных базовых типов используются функции форматированного обмена, прототипы которых:
int fprintf (FILE *stream,"форматная_строка", список_вывода);
int fscanf (FILE *stream,"форматная_строка", список_ввода);
Hапример:
fscanf (in, "%d%d", &a, &b);
fprintf (out, "a=%d b=%d", a, b);
Единственное отличие от функций форматированного ввода-вывода для консоли заключается в добавлении параметра FILE *stream, определяющего поток обмена. При обращении к функциям, это логическое имя файла.
Пример 1. Посимвольное копирование данных из одного файла в другой.
При открытии файлов для ввода и вывода перехват возможной ошибки выполняет условный оператор. Функция perror возвращает сообщение об ошибке, а fprintf переназначает вывод строки сообщения об ошибке в стандартный поток stderr.
# include <stdio.h>
# include <stdlib.h>
void main (void)
{
FILE *in, *out;
// Открыть файлы.
if ((in = fopen ("prim_in.txt", "rt")) == NULL)
{
perror ("Ошибка открытия файла для ввода.");
fprintf (stderr, "Ошибка открытия файла для ввода.\n");
return; // Программа завершит работу.
}
if ((out = fopen("prim_out.txt", "wt")) == NULL)
{
fprintf (stderr, "Ошибка открытия файла для вывода.\n");
return; // Программа завершит работу.
}
// Циклом копирования управляет макроопределение feof.
// Условие выхода «пока входной файл не закончился».
while ( !feof(in) )
fputc (fgetc (in), out); // Выводит в out символ, введенный fgetc(in)
fclose(in);
fclose(out);
}
Пример 2. Построчное копирование данных из одного файла в другой.
Строка прочитывается из одного файла и записывается во второй. При перехвате ошибок открытия файлов, сообщение об ошибке просто выводится на экран, и функция завершает работу. Если между строками чтения из одного файла и записи в другой добавить функцию преобразования строки, то новый файл получит измененный текст.
# include <stdio.h>
# include <conio.h>
void main(void)
{
FILE *in, *out;
char Str[100]; // Переменная для временного хранения строки.
// Открыть файлы.
if ((in = fopen ("prim_in.txt", "rt")) == NULL)
{
printf("Ошибка открытия входного файла\n");
return;
}
if ((out = fopen ("prim_out.txt", "wt")) == NULL)
{
printf("Ошибка открытия входного файла.\n");
return;
}
while (!feof (in)) // Пока в потоке не встречен признак feof
{
fgets (Str, 100, in);
// Здесь можно преобразовать строку.
fputs(Str, out);
}
fclose(in);
fclose(out);
}
Пример 3. Форматный обмен данных.
Используется для обмена с текстовыми файлами, содержащими числовые данные или смешанные, например, строки и числовые данные. При работе с данными файла, чаще всего, их следует хранить в оперативной памяти.
Покажем, как выполнить чтение данных из файла с сохранением в массиве переменной длины. Каждое прочитанное данное сохраняется в отдельном элементе массива. В файле может храниться различное число данных, тогда как длина массива ограничена. При вводе массива его длина может быть определена количеством данных, прочитанных из файла, но очень важно, чтобы все они вошли в адресное пространство, отведенное для массива. Следовательно, цикл ввода должен управляться двумя условиями. Если в файле меньше данных, чем отведено для массива, то цикл завершится условием «найден конец файла», и количество прочитанных данных определит фактическую длину массива. Если в файле больше данных, чем допускает объявление массива, то ввод следует прекратить, как только будет прочитано число данных, соответствующее объявлению. Исходные данные должны быть подготовлены в текстовом файле. После обработки данных их можно записать в тот же файл, для чего следует сначала исходный файл закрыть, затем открыть для добавления, и вывести в него данные.
#include <stdio.h>
void main(void)
{
FILE *in_out;
int mas[100]; // В массиве не более 100 элементов.
int n; // Реальная длина массива.
in_out = fopen("data.txt", "rt");
if (in_out == NULL)
{
perror("Ошибка входного файла.\n");
return;
}
n = 0; // Подготовка ко вводу данных.
// Переменная n используется как рабочая
// для нумерации элементов массива, но при
// завершении цикла она будет знать длину массива.
while ( feof (in_out) = = 0 && n < 100) // Двойное условие
{
fscanf (in_out,"%d", &mas[n]); // Прочитывается один элемент.
printf("%5d", mas[n]); // Вывод на экран.
n++; // Подготовка ко вводу следующего.
}
fclose(in_out); // Файл закрыт.
// Здесь данные массива можно каким-то образом преобразовать.
in_out = fopen("data.txt", "at"); // Файл открыт для добавления.
// Вывод в тот же файл, открытый повторно для добавления.
for (int i = 0; i < n; i++) // Здесь n – уже длина массива.
fprintf (in_out, "%5d", mas[i]);
fclose (in_out);
}
Пример 4. Форматный обмен данных.
В этом примере покажем, как прочитать матрицу из файла. При вводе данных в матрицу, следует помнить, что матрица, в отличие от массива, структура жесткая, ее способ хранения требует, чтобы было известно разбиение на строки. Поэтому не следует пытаться определить программно размер матрицы, а число строк, столбцов следует определить заранее и вписать в текстовый файл в качестве исходных данных. Пусть матрица хранится в файле "matr.txt" в виде:
4 5
1 2 3 4 5
7 6 5 4 3
8 7 5 4 4
8 8 8 6 5
Как видим, на первом месте (в первой строке) записаны два числа, которые определяют размер матрицы, записанной в файле. Далее располагается сама матрица построчно.
#include <stdio.h>
void main(void)
{
FILE *in, *out;
int n, m; // Размер матрицы (строк, столбцов).
int i, j;
int matr[10][10]; // Наибольший допустимый размер.
in = fopen("matr.txt", "rt");
if (in == NULL)
{
perror("Ошибка входного файла.\n");
return;
}
if ((out = fopen("matr_new.txt", "wt")) == NULL)
{
perror("Ошибка выходного файла.\n");
return;
}
// Чтение из файла размера матрицы.
fscanf (in, "%d", &n); // Читается размер матрицы:
fscanf (in, "%d", &m); // n строк, m столбцов.
// После этого управление вводом обычное, как при работе с матрицей.
for (i = 0; i < n; i++)
for (j = 0; j < m; j++)
fscanf (in,"%d", &matr[i][j]); // Прочитывает данные.
// Здесь данные матрицы можно каким-то образом преобразовать.
//Вывод матрицы в файл обычный.
for (i = 0; i < n; i++)
{
for (j = 0; j < m; j++)
fprintf (out,"%6d", matr[i][j]);
fprintf (out, "\n"); // Перевод строки.
}
1.3.7.6. Чтение и запись для бинарных файлов
Двоичный файл хранит информацию в плотном упакованном двоичном представлении. Данные, хранящиеся в таком файле, могут быть произвольного типа, не обязательно базового. Все они имеют одинаковый размер, определенный размером типа данного, пересылаемого в поток. В качестве аргумента указывается void область памяти, обмен происходит сплошным потоком байт. Для обмена с такими файлами используются те же приемы, кроме операций ввода-вывода. Этот обмен не форматированный, поэтому используются функции fread и fwrite, прототипы которых:
size_t fread (void *buf, size_t size, size_t count, FILE *stream)
Функция прочитывает count элементов размером size байт каждый в область памяти, адрес которой определен указателем buf, из файла, определенного указателем stream. Возвращает количество прочитанных элементов, которое может быть меньше, чем count, если произошла ошибка ввода или встречен конец файла.
size_t fwrite (const void *ptr, size_t size, size_t count, FILE *stream)
Функция записывает count элементов размером size байт каждый из области памяти, адрес которой определен указателем buf, в файл, определенный указателем stream. Возвращает количество записанных элементов.
Упрощенно можно записать синтаксис этих функций так:
fread (Адрес_области_ввода, Размер_объекта, Количество_объектов, Имя_файла);
fwrite (Адрес_области_вывода, Размер_объекта, Количество_объектов, Имя_файла);
Например, для обмена двумерного массива данных типа float размером 10*10, можно использовать одно обращение к функции, в результате которого в поток будут переправлены 10*10*4 = 400 байт:
float a[10][10];
..
fread (a, sizeof (float), 10*10, in);
fwrite (a, sizeof (float), 10*10, out);
Приведем в качестве примера текст программы, которая читает данные с консоли, и записывает их в массив. Затем массив сохраняется в файле прямого доступа на диске. При чтении данных с клавиатуры признаком окончания ввода является конец файла, который вводится сочетанием клавиш Ctrl+Z.
#include <stdio.h>
void main(void)
{
FILE *in,*out;
int mas [10];
int len; // Длина массива.
printf ("Введите массив не более 10-ти значений. Окончание ввода – Ctrl+Z\n");
len = 0;
while (!feof (stdin)) // Пока во входном потоке не встречен Ctrl+Z.
scanf ("%d", &mas [len ++]);
// Вывод массива на экран, то есть в поток stdout, это последовательный обмен.
for (int i = 0; i < len; i ++)
printf ("%5d", mas [i]);
if ((out = fopen ("Out.bin", "wb")) == NULL)
{
printf ("Ошибка открытия файла для вывода\n");
return;
}
// Вывод массива в двоичный файл прямого доступа, это прямой обмен.
fwrite (&mas, sizeof (int), len, out);
fclose (out);
if ((in = fopen("Out.bin", "rb")) == NULL)
{
printf("Ошибка открытия файла для ввода\n");
return;
}
// Ввод массива из двоичного файла прямого доступа в новый массив.
int mas_new [10];
fread (&mas_new, sizeof (int), len, in);
fclose (in);
for (i = 0; i < n; i ++)
printf ("%5d", mas_new [i]);
}
Если ввести данные 1, 2, 3, 4, то можно посмотреть, что будет записано в файле Out.bin в текстовом режиме. Содержание файла будет:
☺ ☻ ♥ ♦
1.3.7.7. Позиционирование в потоке
Для файлов прямого доступа, поскольку все записи одного размера, имеется возможность ввода-вывода произвольной записи. Напомним, что любая операция чтения (записи) для потока всегда производитcя, начиная с текущей позиции. Начальная позиция чтения/записи в потоке устанавливается при открытии потока и может соответствовать начальному или конечному байту потока в зависимости от режима открытия потока. При открытии потока в режимах "r" и "w" указатель текущей позиции чтения/записи в потоке устанавливается на начальный байт потока, а при открытии в режиме "а" в конец файла (за последним байтом). При выполнении каждой операции ввода-вывода указатель текущей позиции перемещается на новую текущую позицию в соответствии с числом прочитанных (записанных) байтов.
Средства позиционирования в потоке позволяют перемещать указатель потока непосредственно на нужный байт, что позволяет работать с файлом на диске, как с обычным массивом, осуществляя доступ к содержимому файла в произвольном порядке. Эти функции, и многие другие, входят в библиотеку <stdio.h>. Для перемещения указателя потока используется функция fseek( ), прототип которой:
int fseek (FILE *указатель_на_поток, long смещение, int начало_отсчета)\
Смещение задается переменной или выражением типа long и может быть отрицательным, т.е. возможно перемещение по файлу в прямом и обратном направлениях. Начало отсчета задается одной из предопределенных констант:
SEEK_SET (имеет значение 0) – начало файла;
SEEK_CUR (имеет значение 1) –текущая позиция;
SEEK_END (имеет значение 2) – конец файла.
Функция fseek() возвращает 0, если перемещение в потоке (файле) выполнено успешно, иначе возвращается ненулевое значение.
Перемещение к началу потока (файла) из произвольной позиции:
fseek(fp, 0L, SEEK_SET);
Перемещение к концу потока (файла) из произвольной позиции:
fseek(fp, 0L, SEEK_END);
При использовании производных типов данных (таких, как структура) можно перемещаться в потоке (файле) на то количество байтов, которое занимает этот тип данных. Так, если определена структура:
struct str
{
...
} st;
то при следующем обращении к функции fseek() указатель текущей позиции в потоке будет перемещен на одну структуру назад относительно текущей позиции:
fseek (fp, – (long) sizeof (st) , SEEK_CUR) ;
Здесь пример.
Кроме рассмотренной функции fseek(), в библиотеке <stdio.h> есть следующие функции для работы с указателями текущей позиции в потоке:
long int ftell (FILE *f)
Возвращает значение указателя текущей позиции в файле, связанном с потоком f, как длинное целое.
void rewind (FILE *f)
Перемотка. Функция очищает флаги ошибок в потоке f и устанавливает указатель текущей позиции на начало файла.
int fgetpos (FILE *f, fpos_t *pos)
Возвращает указатель текущей позиции в файле, связанном с потоком f, и копирует значение по адресу pos. Возвращаемое значение имеет тип fpos_t. Значение после может использоваться функцией fsetpos.
int fsetpos (FILE *f, const fops_t *pos)
Перемещает указатель текущей позиции в файле, связанном с потоком f, относительно его начала на позицию *pos.
Необходимо иметь в виду, что недопустимо использовать функции работы с указателем текущей позиции в потоке для потока, связанного не с файлом, а с устройством. Поэтому применение описанных выше функций с любым из стандартных потоков приводит к неопределенным результатам.
1.3.7.8. Функции работы с потоком
Перечислим некоторые функции библиотеки <stdio.h> для работы с файлами.
int ferror (FILE *f)
Возвращает код ошибки при работе с потоком. Это целое число, означающее код ошибки. Если ошибки нет, 0.
int fflush (FILE *f)
Проталкивание буфера. Посылает данные из буфера вывода для немедленной записи в файл. Возвращает 0 при успешном завершении, иначе eof.
FILE * freopen (const char *fname, const char *mode, FILE *f)
Открывает поток ввода-вывода. Работает аналогично fopen, но предварительно закрывает поток f, если тот был открыт.
int remove (const *char fname)
Удаляет существующий файл. Предварительно файл должен быть закрыт. При успехе возвращает 0, иначе ненулевое значение.
int rename (const *char Old_name, const *char New_name)
Переименовывает существующий файл или каталог. Предварительно файл должен быть закрыт. При успехе возвращает 0, иначе ненулевое значение
void setbuf (FILE *f, char *p)
Управляет буфером ввода-вывода. Может установить буфер ввода-вывода, заданный указателем p.
FILE *tmpfile (void)
Открывает поток двоичного ввода-вывода во временный файл, возвращает указатель на поток.
char *tmpnam (char *S)
Создает уникальное имя файла, которое может быть использовано как имя временного файла.
|