Программирование на языке С++ в среде Qt Creator
Алексеев Евгений Ростиславович, Злобин Григорий Григорьевич, Костюк Дмитрий Александрович, Чеснокова Оксана Витальевна, Чмыхало Александр Сергеевич

Содержание


Лекция 1. Предисловие

В этой главе читатель напишет свои первые программы на языке С(С++), по- знакомится с основными этапами перевода программы с языка C++ в машинный код. Второй параграф главы посвящён знакомству со средой Qt Creator.

Предисловие. Знакомство с языком С++

Книга, которую открыл читатель, является с одной стороны учебником по алгоритмизации и программированию на C++, а с другой — пособием по разработке визуальных приложений в среде Qt Creator. В книге описаны среда программирования Qt Creator и редактор Geany. При чтении книги не требуется предварительного знакомства с программированием.

В первой части книги (главы 1–9) на большом количестве примеров представлены методы построения программ на языке C++, особое внимание уделено построению циклических программ, программированию с использованием функций, массивов, матриц и указателей.

Вторая часть книги (глава 10) посвящена объектно-ориентированному программированию на C++.

В третьей части книги (главы 11–15) читатель научится создавать кроссплатформенные визуальные приложения с помощью Qt Creator и познакомится с библиотекой классов Qt.

В книге присутствуют задания для самостоятельного решения.

В приложениях описан текстовый редактор Geany, а также кроссплатформенная библиотека MathGL, предназначенная для построения различных двух- и трёхмерных графиков.

Главы 1–9 написаны Е. Р. Алексеевым и О. В. Чесноковой. Автором раздела по объектно-ориентированному программированию является Д. А. Костюк. Главы 11–15, посвящённые программированию с использованием инструментария Qt, написаны Г. Г. Злобиным и А. C. Чмыхало.

Авторы благодарят компанию ALT Linux (www.altlinux.ru) и лично Алексея Смирнова и Владимира Чёрного за возможность издать очередную книгу по свободному программному обеспечению.

Знакомство с языком С++

В этой главе читатель напишет свои первые программы на языке С(С++), познакомится с основными этапами перевода программы с языка C++ в машинный код. Второй параграф главы посвящён знакомству со средой Qt Creator.

1.1 Первая программа на C++

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

Задача 1.1. Заданы две стороны прямоугольника a, b. Найти его площадь и периметр.

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

1 #include <iostream>
2 using namespace std;
3 int main( )
4 {
5	float a, b, s, p;
6	cout<<"a=";
7	cin>>a;
8	cout<<"b=";
9	cin>>b;
10	p=2*(a+b );
11	s=a*b;
12	cout << "Периметр прямоугольника равен " << p <<endl;
13	cout << "Площадь прямоугольника равна " << s <<endl;
14	return 0;
15 }

Давайте построчно рассмотрим текст программы и познакомимся со структурой программы на С++ и с некоторыми операторами языка.

Строка 1. Указывает компилятору (а точнее, препроцессору), что надо использовать функции из стандартной библиотеки iostream. Библиотека iostream нужна для организации ввода с помощью инструкции cin и вывода — с помощью cout. В программе на языке C++ должны быть подключены все используемые библиотеки.

Строка 2. Эта строка обозначает, что при вводе и выводе с помощью cin и cout будут использоваться стандартные устройства (клавиатура и экран), если эту строку не указывать, то каждый раз при вводе вместо cin надо будет писать std::cin, а вместо cout –- std::cout.

Строка 3. Заголовок главной функции (главная функция имеет имя main). В простых программах присутствует только функция main().

Строка 4. Любая функция начинается с символа {.

Строка 5. Описание вещественных (float) переменных a (длина одной стороны прямоугольника), b (длина второй стороны прямоугольника), s (площадь прямоугольника), p (периметр прямоугольника). Имя переменной1)состоит из латинских букв, цифр и символа подчёркивания. Имя не может начинаться с цифры. В языке С++ большие и малые буквы различимы. Например, имена PR_1, pr_1, Pr_1 и pR_1 — разные.

Строка 6. Вывод строки символов a= с помощью cout. Программа выведет подсказку пользователю, что необходимо вводить переменную a

Строка 7. Ввод вещественного числа a с помощью cin. В это момент программа останавливается и ждёт, пока пользователь введёт значение переменой a с клавиатуры.

Строка 8. Вывод строки символов b= с помощью cout.

Строка 9. Ввод вещественного числа b с помощью cin.

Строка 10. Оператор присваивания для вычисления периметра прямоугольника (переменная p) по формуле . В операторе присваивания могут использоваться круглые скобки и знаки операций: + (сложение), - (вычитание), * (умножение), / (деление).

Строка 11. Оператор присваивания для вычисления площади прямоугольника.

Строка 12. Вывод строки "Периметр прямоугольника равен " и значения p на экран. Константа endl хранит строку "\n", которая предназначена для перевода курсора в новую строку дисплея2).Таким образом строка

cout <<"Периметр прямоугольника равен "<< p <<endl;
			

выводит на экран текст "Периметр прямоугольника равен "3), значение переменной p, и переводит курсор в новую строку.

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

Строка 14. Оператор return, который возвращает значение в операционную систему. Об этом подробный разговор предстоит в п. 4.9.Сейчас следует запомнить: если программа начинается со строки int main(), последним оператором должен быть4)return 0.

Строка 15. Любая функция (в том числе и main) заканчивается символом }.

Мы рассмотрели простейшую программу на языке С++, состоящую из операторов ввода данных, операторов присваивания (в которых происходит расчёт по формулам) и операторов вывода.

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

Директивы препроцессора
Объявление глобальных переменных
Тип_результата f1 (Список_переменных)
{
Операторы
}
Тип_результата f2 (Список_переменных)
{
Операторы
}
...
Тип_результата fn (Список_переменных)
{
Операторы
}
Тип_ результата main (Список_переменных)
{
Операторы
}
			

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

Директивы препроцессора
Тип_ результата main (Список_переменных)
{
Операторы
}
			

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

Процесс перевода программы в машинный код называется трансляцией. Если в качестве транслятора выступает компилятор, то используют термин компиляция программы. При переводе программы с языка С++ в машинный код используются именно компиляторы, и поэтому применительно к языку С++ термины "компилятор" и "транслятор" эквивалентны.

Рассмотрим основные этапы обработки компилятором программы на языке С++ и формирования машинного кода.

  1. Сначала с программой работает препроцессор5), он обрабатывает директивы, в нашем случае это директивы включения заголовочных файлов (файлов с расширением .h) — текстовых файлов, в которых содержится описание используемых библиотек. В результате формируется полный текст программы, который поступает на вход компилятора.
  2. Компилятор разбирает текст программ на составляющие элементы, проверяет синтаксические ошибки и в случае их отсутствия формирует объектный код (файл с расширением .o или .obj). Получаемый на этом этапе двоичный код не включает в себя двоичные коды библиотечных функций и функций пользователя.
  3. Компоновщик подключает к объектному коду программы объектные модули библиотек и других файлов (если программа состоит из нескольких фай-лов) и генерирует исполняемый код программы (двоичный файл), который уже можно запускать на выполнение. Этот этап называется компоновкой или сборкой программы.

После написания программы её необходимо ввести в компьютер. В той книге будет рассматриваться работа на языке C++ в среде Qt Creator6). Поэтому перед вводом программы в компьютер надо познакомиться со средой программирова-ния.

1.2 Среда программирования Qt Creator

Среда программирования Qt Creator (IDE QT Creator) находится в репози-тории большинства современных дистрибутивов Linux (OC Linux Debian, OC Linux Ubuntu, OC ROSA Linux, ALT Linux и др.). Установка осуществляется штатными средствами вашей операционной системы (менеджер пакетов Synaptic и др.) из репозитория, достаточно установить пакет qtcreator, необходимые па-кеты и библиотеки будут доставлены автоматически. Последнюю версию IDE Qt Creator можно скачать на сайте QtProject (http://qt-project.org/downloads). Установочный файл имеет расширение .run. Для установки приложения, необходимо запустить его на выполнение. Установка проходит в графическом режиме. После запуска программы пользователь увидит на экране окно, подобное представленному на рис. 1.11).

Окно Qt Creator


увеличить изображение

Рис. 1.1.  Окно Qt Creator

При работе в Qt Creator вы находитесь в одном из режимов:

  1. Welcome (Начало) — отображает экран приветствия, позволяя быстро загружать недавние сессии или отдельные проекты. Этот режим можно увидеть при запуске Qt Creator без указания ключей командной строки.
  2. Edit (Редактор) — позволяет редактировать файлы проекта и исходных кодов. Боковая панель слева предоставляет различные виды для перемещения между файлами.
  3. Debug (Отладка) — предоставляет различные способы для просмотра состояния программы при отладке.
  4. Projects (Проекты) — используется для настройки сборки, запуска и редактирования кода.
  5. Analyze (Анализ) — в Qt интегрированы современные средства анализа кода разрабатываемого приложения.
  6. Help (Справка) — используется для вывода документации библиотеки Qt и Qt Creator.
  7. Output (Вывод) — используется для вывода подробных сведений о проекте.

Рассмотрим простейшие приёмы работы в среде Qt Creator на примере создания консольного приложения для решения задачи 1.1. Для этого можно поступить одним из способов:

  1. В меню File (Файл) выбрать команду New File or Project (Новый файл или проект) (комбинация клавиш Ctrl+N).
  2. Находясь в режиме Welcome (Начало) главного окна QtCreator (рис. 1.1) щёлкаем по ссылке Develop (Разработка) и выбираем команду Create Project (Создать проект).

После это откроется окно, подобное представленному на рис. 1.2. Для создания простейшего консольного приложения выбираем Non-Qt Project (Проект без использования Qt) — Plain C++ Project (Простой проект на языке С++).

Далее выбираем имя проекта и каталог для его размещения (см. рис. 1.3)2). Следующие два этапа создания нашего первого приложения оставляем без изменения3). После чего окно IDE Qt Creator будет подобно представленному на рис. 1.4. Заменим текст "Hello, Word" стандартного приложения, на текст программы решения задачи 1.1.

Окно выбора типа приложения в Qt Creator


увеличить изображение

Рис. 1.2.  Окно выбора типа приложения в Qt Creator

Для сохранения текста программы можно воспользоваться командой Сохранить или Сохранить всё из меню Файл. Откомпилировать и запустить программу можно одним из следующих способов:

  1. Пункт меню Сборка-Запустить.
  2. Нажать на клавиатуре комбинацию клавиш Ctrk+R.
  3. Щёлкнуть по кнопке Запустить ( ).

Окно с результатами работы программы представлено на рис. 1.5.

Выбор имени и каталога нового проекта


увеличить изображение

Рис. 1.3.  Выбор имени и каталога нового проекта

Главное окно создания консольного приложения


увеличить изображение

Рис. 1.4.  Главное окно создания консольного приложения

Авторы сталкивались с тем, что в некоторых дистрибутивах Ubuntu Linux и Linux Mint после установки Qt Creator не запускались консольные приложения. Если читатель столкнулся с подобной проблемой, скорее всего надо корректно настроить терминал, который отвечает за запуск приложений в консоли. Для этого вызываем команду Tools — Options — Environment (см. рис. 1.6). Параметр Terminal (Терминал) должен быть таким же, как показано на рис. 1.6. Проверьте установлен ли в Вашей системе пакет xterm, и при необходимости доставьте его. После этого не должно быть проблем с запуском консольных приложений.

Аналогичным образом можно создавать и запускать любое консольное приложение.

Результаты работы программы решения задачи 1.1


Рис. 1.5.  Результаты работы программы решения задачи 1.1

Окно настроек среды Qt Creator


увеличить изображение

Рис. 1.6.  Окно настроек среды Qt Creator

Дальнейшее знакомство со средой Qt Creator продолжим, решая следующую задачу.

Задача 1.2. Заданы длины трёх сторон треугольника и (см. рис. 1.7). Вычислить площадь и периметр треугольника.

Для решения задачи можно воспользоваться формулой Герона , где — периметр.

Решение задачи можно разбить на следующие этапы:

  1. Определение значений и (ввод величин с клавиатуры в память компьютера).
  2. Расчёт значений и по приведённым выше формулам.
  3. Вывод и на экран дисплея.

Ниже приведён текст программы. Сразу заметим, что в тексте могут встречаться строки, начинающие с двух наклонных (//). Это комментарии. Комментарии не являются обязательными элементами программы и ничего не сообщают компьютеру, они поясняют человеку, читающему текст программы, назначение отдель ных элементов программы. В книге комментарии будут широко использоваться для пояснения отдельных участков программы.

Треугольник


Рис. 1.7.  Треугольник

#include <iostream>
#include <math.h>
using namespace std;
int main( )
{
float a, b, c, s, p;
cout<<"Введите длины сторон треугольника"<<endl;
//Ввод значений длин треугольника a, b, c .
cin>>a>>b>>c;
//Вычисление периметра треугольника .
p=a+b+c;
//Вычисление площади треугольника .
s=sqrt ( p /2*( p/2-a )*( p/2-b )*( p/2-c ) );
//Вывод на экран дисплея значений площади и периметра треугольника.
cout<<"Периметр треугольника равен "<<p<<", его площадь равна "<<s<<endl;
return 0;
}
			

Кроме используемой в предыдущей программе библиотеки iostream, в строке 2 подключим библиотеку math.h, которая служит для использования математических функций языка С(С++). В данной программе используется функция извлечения квадратного корня — . Остальные операторы (ввода, вывода, вычисления значений переменных) аналогичны используемым в предыдущей программе.

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

Лекция 2. Общие сведения о языке С++

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

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

Программа на языке С++ может содержать следующие символы:

Из символов алфавита формируют ключевые слова и идентификаторы. Ключевые слова — это зарезервированные слова, которые имеют специальное значение для компилятора и используются только в том смысле, в котором они определены (операторы языка, типы данных и т.п.). Идентификатор — это имя программного объекта, представляющее собой совокупность букв, цифр и символа подчёркивания. Первый символ идентификатора — буква или знак подчёркивания, но не цифра. Идентификатор не может содержать пробел. Прописные и строчные буквы в именах различаются, например, ABC, abc, Abc — три различных имени. Каждое имя (идентификатор) должно быть уникальным в пределах функции и не совпадать с ключевыми словами.

В тексте программы можно использовать комментарии. Если текст начинается с двух символов "косая черта" // и заканчивается символом перехода на новую строку или заключён между символами /* и */, то компилятор его игнорирует.

/* Комментарий может
выглядеть так! */
//А если вы используете такой способ,
//то каждая строка должна начинаться
//с двух символов "косая черта".
		

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

2.2 Данные

Для решения задачи в любой программе выполняется обработка каких-либо данных. Данные хранятся в памяти компьютера и могут быть самых различных типов: целыми и вещественными числами, символами, строками, массивами. Типы данных определяют способ хранения чисел или символов в памяти компьютера. Они задают размер ячейки, в которую будет записано то или иное значение, определяя тем самым его максимальную величину или точность задания. Участок памяти (ячейка), в котором хранится значение определённого типа, называется переменной. У переменной есть имя (идентификатор) и значение. Имя служит для обращения к области памяти, в которой хранится значение. Во время выполнения программы значение переменной можно изменить. Перед использованием любая переменная должна быть описана. Оператор описания переменных в языке С++ имеет вид:

тип имя_переменной;
		

или

тип список_переменных;
		

Типы данных языка С++ можно разделить на основные и составные.

К основным типам данных языка относят:

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

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

2.2.1 Символьный тип

Данные типа char в памяти компьютера занимают один байт1).Это связано с тем, что обычно под величину символьного типа отводят столько памяти, сколько необходимо для хранения любого из 256 символов клавиатуры. Символьный тип может быть со знаком или без знака (табл. 2.1).

Таблица 2.1. Символьные типы данных
ТипДиапазонРазмер
char–128... 1271 байт
unsigned char0... 2551 байт
signed char–128... 1271 байт

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

char c, str; //Описаны две символьные переменные.
			

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

Например, 'a', 'b', '+', '3'.

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

2.2.2 Целочисленный тип

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

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

Таблица 2.2. Целые типы данных
ТипДиапазонРазмер
int–2147483647...21474836474 байта
unsigned int0...42949672954 байта
signed int–2147483647...21474836474 байта
short int–32767...327672 байта
long int–2147483647...21474836474 байта
unsigned short int0...655352 байта
signed short int–32767...327672 байта
long long int–(263–1)...(263–1)8 байт
signed long int–2147483647...21474836474 байта
unsigned long int0...42949672954 байта
unsigned long long int0...264–18 байт

Пример описания целочисленных данных:

int a, b, c;
unsigned long int A, B, C;
			

2.2.3 Вещественный тип

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

Обычно величины типа float занимают 4 байта, из которых один двоичный разряд отводится под знак, 8 разрядов под порядок и 23 под мантиссу. Поскольку старшая цифра мантиссы всегда равна 1, она не хранится.

Величины типа double занимают 8 байт, в них под порядок и мантиссу отводится 11 и 52 разряда соответственно. Длина мантиссы определяет точность числа, а длина порядка его диапазон. Спецификатор типа long перед именем типа double указывает, что под величину отводится 10 байт.

Диапазоны значений вещественного типа представлены в табл. 2.3.

Таблица 2.3. Вещественные типы данных
ТипДиапазонРазмер
float3.4Е-38...3.4E+384 байта
double1.7Е-308...1.7E+3088 байт
long double3.4Е-4932...3.4E+493210 байт

Пример описания вещественных переменных:

double x1, x2, x3;
float X, Y, Z;
			

2.2.4 Логический тип

Переменная типа bool может принимать только два значения true (истина) или false (ложь). Любое значение не равное нулю интерпретируется как true, а при преобразовании к целому типу принимает значение равное 1. Значение false представлено в памяти как 0.

Пример описания данных логического типа:

bool F, T;
			

2.2.5 Тип void

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

2.3 Константы

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

сonst тип имя_константы = значение;
		

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

const double pi=3.141592653589793;
		

Кроме того, константа может быть определена с помощью директивы1) #define. Эта директива служит для замены часто использующихся констант, ключевых слов, операторов или выражений некоторыми идентификаторами. Идентификаторы, заменяющие текстовые или числовые константы, называют именованными константами. Основная форма синтаксиса директивы следующая:

#define идентификатор текст
		

Например,

#define PI 3.141592653589793
int main( )
...
		

2.4 Структурированные типы данных

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

Массив — совокупность данных одного и того же типа2).Число элементов массива фиксируется при описании типа и в процессе выполнения программы не изменяется.

В общем виде массив можно описать так:

тип имя [размерность_1][размерность_2]...[размерность_N];
		

Например,

float x 10]; //Описан массив из 10 вещественных чисел.
int a[3][4]; //Описан двумерный целочисленный массив, матрица из 3-х строк и 4-х столбцов.
double b[2][3][2]; //Описан трехмерный массив.
		

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

имя_массива[номер_1][номер_2]...[номер_N]
		

Например:

x[5], a[2][3], b[1][2][2] .
		

Элементы массива в С++ нумеруются с нуля. Первый элемент, всегда имеет номер ноль, а номер последнего элемента на единицу меньше заданной при его описании размерности:

char C[5]; //Описан массив из 5 символов, нумерация от 0 до 4.
		

Строка — последовательность символов3).В С++ строки описывают как массив элементов типа char. Например:

char s[25]; //Описана строка из 25 символов.
		

Структура4) это тип данных, который позволяет объединить разнородные данные и обрабатывать их как единое целое.

Например

struct fraction //Объявлена структура правильная дробь.
{
//Определяем поля структуры:
int num; //поле числитель,
int den; //поле знаменатель.
}
...
fraction d, D [ 2 0 ]; //Определена переменная d, массив D[20], типа fraction.
...
d. num; //Обращение к полю num переменной d.
D[4]. den; //Обращение к полю den четвёртого элемента массива D.
		

2.5 Указатели

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

Как правило, при обработке оператора объявления переменной

тип имя_переменной;
		

компилятор автоматически выделяет память под переменную имя_переменной в соответствии с указанным типом:

char C; //Выделена память под символьную переменную C
		

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

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

Итак, указатель это переменная, значением которой является адрес памяти, по которому хранится объект определённого типа (другая переменная). Например, если С это переменная типа char, а Р — указатель на С, значит в Р находится адрес по которому в памяти компьютера хранится значение переменной С.

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

тип *имя_переменной;
		

Например:

int *p; //По адресу, записанному в переменной p, будет храниться переменная типа int
		

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

float *x, y, *z; //Описаны указатели на вещественные числа x и z (сами вещественные
//значения — *x, *z), а так же вещественная переменная y, её адрес — &y.
		

2.6 Операции и выражения

Выражение задаёт порядок выполнения действий над данными и состоит из операндов (констант, переменных, обращений к функциям), круглых скобок и знаков операций. Операции делятся на унарные (например, ) и бинарные (например, а+b). В таблице 2.4 представлены основные операции языка С++.

Таблица 2.4. Основные операции языка С++
ОперацияОписание
Унарные операции
++увеличение значения на единицу
--уменьшение значения на единицу
~поразрядное отрицание
!логическое отрицание
-арифметическое отрицание (унарный минус)
+унарный плюс
&получение адреса
*обращение по адресу
(type)преобразование типа
Бинарные операции
+сложение
-вычитание
*умножение
/деление
%остаток от деления
<<сдвиг влево
>>сдвиг вправо
<меньше
>больше
<=меньше или равно
>=больше или равно
==равно
!=не равно
&поразрядная конъюнкция (И)
^поразрядное исключающее ИЛИ
|поразрядная дизъюнкция (ИЛИ)
&&логическое И
||логическое ИЛИ
=присваивание
*=умножение с присваиванием
/=деление с присваиванием
+=сложение с присваиванием
-=вычитание с присваиванием
%=остаток от деления с присваиванием
<<=сдвиг влево с присваиванием
>>=сдвиг вправо с присваиванием
&=поразрядная конъюнкция с присваиванием
|=поразрядная дизъюнкция с присваиванием
^=поразрядное исключающее ИЛИ с присваиванием
Другие операции
?условная операция
,последовательное вычисление
sizeofопределение размера
(тип)преобразование типа

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

2.6.1 Операции присваивания

Обычная операция присваивания имеет вид:

имя_переменной=значение;
			

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

Например,

b=3; //Переменной b присваивается значение, равное трём.
a=b; //Переменной а присваивается значение b.
c=a+2*b; //Переменной c присваивается значение выражения.
c=c +1; //Значение переменой с увеличивается на единицу.
a=a *3; //Значение переменой а увеличивается в три раза.
			

Задача 2.1. Пусть в переменной а хранится значение, равное 3, а в переменную b записали число 5. Поменять местами значения переменных а и b.

Для решения задачи понадобится дополнительная переменная c(см. рис. 2.1). В ней временно сохраняется значение переменной а. Затем, значение переменной bзаписывается в переменную a, а значение переменной c в переменную b.

c=a; //Шаг 1. с=3
a=b; //Шаг 2. a=5
b=c; //Шаг 3. b=3
			

Использование буферной переменной


Рис. 2.1.  Использование буферной переменной

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

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

имя_1 = имя_2 = ... = имя_N = значение;
			

Запись a=b=c=3.14159/6; означает, что переменным a, b и c было присвоено одно и то же значение 3.14159/6.

Операции + =, -=, *=, / = называют составным присваиванием. В таких операциях при вычислении выражения стоящего справа используется значение переменной из левой части, например так:

x+=p; //Увеличение x на p, то же что и x=x+p.
x-=p; //Уменьшения x на p, то же что и x=x-p.
x*=p; //Умножение x на p, то же что и x=x*p.
x/=p; //Деление x на p, то же что и x=x/p.
			

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

Операции +, -, *, / относят к арифметическим операциям. Их назначение понятно и не требует дополнительных пояснений. При программировании арифметических выражений следует придерживаться простых правил. Соблюдать очерёдность выполнения арифметических операций. Сначала выполняются операции умножения и деления (1-й уровень), а затем сложения и вычитания (2-й уровень). Операции одного уровня выполняются последовательно друг за другом. Для изменения очерёдности выполнения операций используют скобки. Таблица 2.5 содержит примеры записи алгебраических выражений.

Таблица 2.5. Примеры записи алгебраических выражений
Математическая записьЗапись на языке С++
2*a+b*(c+d)
3*(a+b)/(c+d)
(3*a-2*b)/(c*d) или (3*a-2*b)/c/d
(b-a)*(b-a)/(c+1/(d-2))-(a*a+1)/(b*b+c*d)

Операции инкремента ++ и декремента -- так же причисляют к арифметическим, так как они выполняют увеличение и уменьшение на единицу значения переменной. Эти операции имеют две формы записи: префиксную (операция записывается перед операндом) и постфиксную (операция записывается после операнда). Так, например оператор p=p+1; можно представить в префиксной форме ++p; и в постфиксной p++;.Эти формы отличаются при использовании их в выражении. Если знак декремента (инкремента) предшествует операнду, то сначала выполняется увеличение (уменьшение) значения операнда, а затем операнд участвует в выражении. Например,

x=12;
y=++x; //В переменных x и y будет храниться значение 13.
			

Если знак декремента (инкремента) следует после операнда, то сначала операнд участвует в выражении, а затем выполняется увеличение (уменьшение) значения операнда:

x=12;
y=x++; //Результат — число 12 в переменной y, а в x — 13.
			

Остановимся на операциях целочисленной арифметики.

Операция целочисленного деления / возвращает целую часть частного (дробная часть отбрасывается) в том случае, если она применяется к целочисленным операндам, в противном случае выполняется обычное деление: 11/4 = 2 или 11.0/4 = 2.75.

Операция остаток от деления % применяется только к целочисленным операндам: 11%4 = 3.

К операциям битовой арифметики относятся следующие операции: &, |, ^, ~, <<, >>. В операциях битовой арифметики действия происходят над двоичным представлением целых чисел.

Арифметическое И (&). Оба операнда переводятся в двоичную систему, затем над ними происходит логическое поразрядное умножение операндов по следующим правилам:

1&1=1, 1&0=0, 0&1=0, 0&0=0.
			

Например, если А=14 и В=24, то их двоичное представление — А=0000000000001110 и В=0000000000011000. В результате логического умножения A and B получим 0000000000001000 или 8 в десятичной системе счисления (рис. 2.2). Таким образом, A&B=14&24=8.

Пример логического умножения


Рис. 2.2.  Пример логического умножения

Пример логического сложения


Рис. 2.3.  Пример логического сложения

Арифметическое ИЛИ (|). Здесь также оба операнда переводятся в двоичную систему, после чего над ними происходит логическое поразрядное сложение операндов по следующим правилам:

1|1=1, 1|0=1, 0|1=1, 0|0=0.
			

Например, результат логического сложения чисел А=14 и В=24 будет равен A |B=30 (рис. 2.3).

Арифметическое исключающее ИЛИ (^). Оба операнда переводятся в двоичную систему, после чего над ними происходит логическая поразрядная операция ^ по следующим правилам:

1^1=0, 1^0=1, 0^1=1, 0^0=0.
			

Арифметическое отрицание (~). Эта операция выполняется над одним операндом. Применение операции ~ вызывает побитную инверсию двоичного представления числа (рис. 2.4).

Пример арифметического отрицания


Рис. 2.4.  Пример арифметического отрицания

Сдвиг влево (M<<L). Число M, представленное в двоичной системе, сдвигается влево на Lпозиций. Рассмотрим операцию 15 << 3. Число 15 в двоичной системе имеет вид 1111. При сдвиге его на 3 позиции влево получим 1111000. В десятичной системе это двоичное число равно 120. Итак, 15 << 3 =120(рис. 2.5). Заметим, что сдвиг на один разряд влево соответствует умножению на два, на два разряда — умножению на четыре, на три — умножению на восемь. Таким образом, операция M << Lэквивалентна умножению числа M на 2 в степени L.

Пример операции "Сдвиг влево"


Рис. 2.5.  Пример операции "Сдвиг влево"

Пример операции "Сдвиг вправо"


Рис. 2.6.  Пример операции "Сдвиг вправо"

Сдвиг вправо (M>>L). Число M, представленное в двоичной системе, сдвигается вправо на L позиций, что эквивалентно целочисленному делению числа M на 2 в степени L. Например, 15 >> 1=7 (рис. 2.6), 15 >> 3= 2.

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

В С++ определены следующие логические операции || (или), && (и), ! (не). Логические операции выполняются над логическими значениями true (истина) и false (ложь). В языке С ложь — 0, истина — любое значение 60. В таблице 2.6 приведены результаты логических операций.

Таблица 2.6. Логические операции
AB!AA&&BA||B
00100
01101
10001
11011

2.6.4 Операции отношения

Операции отношения возвращают в качестве результата логическое значение. Таких операций шесть: >, >=, <, <=, ==, !=. Результат операции отношения — логическое значение true (истина) или false (ложь).

2.6.5 Условная операция

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

условие ? выражение1 : выражение2;
			

Работает операция следующим образом. Если условие истинно (не равно 0), то результатом будет выражение1, в противном случае выражение2. Например, операция y=x<0 ? -x : x; записывает в переменную y модуль числа х.

2.6.6 Операция преобразования типа

Для приведения выражения к другому типу данных в С++ существует операция преобразования типа:

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

Например, в результате действий x=5; y=x/2; z=(float) x/2; переменная y примет значение равное 2 (результат целочисленного деления), а переменная z = 2.5.

2.6.7 Операция определения размера

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

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

Например, предположим, что была описана целочисленная переменная int k=3;.Исходя из того, что тип int занимает в памяти 4 байта, в переменную m=sizeof k; будет записано число 4.

В результате работы команд double z=123.456; p=sizeof (k+z); значение переменной p стало равно 8, т. к. вещественный тип double более длинный (8 байтов) по сравнению с типом int(4 байта) и значение результата было преобразовано к более длинному типу. В записи операции sizeof (k+z) были использованы скобки. Это связано с тем, что операция определения типа имеет более высокий приоритет, чем операция сложения. При заданном значении z=123.456; та же команда, но без скобок p=sizeof k+z; вычислит p=4+123.456=127.456.

Команда s = sizeof "Hello"; определит, что под заданную строку в памяти было выделено s=6 байтов, т. к. объект состоит из 5 символов и один байт на символ окончания строки.

2.6.8 Операции с указателями

При работе с указателями часто используют операции получения адреса & и разадресации * (табл. 2.7).

Таблица 2.7. Операции получения адреса & и разадресации *
ОписаниеАдресЗначение, хранящееся по адресу
тип *pp*p
тип p&pp

Операция получения адреса & возвращает адрес своего операнда. Например:

float a; //Объявлена вещественная переменная а
float *adr_a; //Объявлен указатель на тип float
adr_a=&a; //Оператор записывает в переменную adr_a адрес переменной a
			

Операция разадресации * возвращает значение переменной, хранящееся по заданному адресу, т.е. выполняет действие, обратное операции &:

float a;	//Объявлена вещественная переменная а.
float *adr_a;	//Объявлен указатель на тип float.
a=*adr_a;	//Оператор записывает в переменную a вещественное значение,
//хранящееся по адресу adr_a.
			

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

//Описана вещественная переменная и два указателя.
float PI =3.14159,*p1, *p2;
//В указатели p1 и p2 записывается адрес переменной PI.
p1=p2=&PI;
			

Если указатели ссылаются на различные типы, то при присваивании значения одного указателя другому, необходимо использовать преобразование типов. Без преобразования можно присваивать любому указателю указатель void*.

Рассмотрим пример работы с указателями различных типов:

float PI = 3.14159;	//Объявлена вещественная переменная.
float *p1;	//Объявлен указатель на float.
double *p2;	//Объявлен указатель на double.
p1=&PI;	//Переменной p1 присваивается значение адреса PI.
p2=(double *) p1;	//Указателю на double присваивается значение,
//которое ссылается на тип float.
			

В указателях p1 и p2 хранится один и тот же адрес (p1=0012FF7C), но значения, на которые они ссылаются разные (*p1=3.14159, *p2=2.642140e-308). Это связано с тем, указатель типа *float адресует 4 байта, а указатель *double — 8 байт. После присваивания p2=(double *)p1; при обращении к *p2 происходит следующее: к переменной, хранящейся по адресу p1, дописывается ещё следующих 4 байта из памяти. В результате значение *p2 не совпадает со значением *p1.

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

Над адресами С++ определены следующие арифметические операции:

Сложение и вычитание указателей с константой n означает, что указатель перемещается по ячейкам памяти на столько байт, сколько занимает n переменных того типа, на который он указывает. Например, пусть указатель имеет символьный тип, и его значение равно 100. Результат сложения этого указателя с единицей — 101, так как для хранения переменной типа char требуется один байт. Если же значение указателя равно 100, но он имеет целочисленный тип, то результат его сложения с единицей будет составлять 104, так как для переменной типа int отводится четыре байта.

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

Фактически получается, что значение указателя изменяется на величину sizeof(тип).Если указатель на определённый тип увеличивается или уменьшается на константу, то его значение изменяется на величину этой константы, умноженную на размер объекта данного типа. Например:

//Объявление массива из 10 элементов.
double mas [10] = { 1.29,3.23,7.98,5.54,8.32,2.48,7.1 };
double *p1;	//Объявление указателя на double
p1=&mas [ 0 ];	//Присвоение указателю адреса нулевого элемента массива.
p1=p1+3;	//Увеличение значения адреса на 3*8=24 (размер типа double), в результате указатель
//сместится на три ячейки, размером double каждая.
			

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

Операции инкремента и декремента, соответственно, увеличивают или уменьшают значение адреса:

double *p1;
float *p2;
int *i;
p1++; //Увеличение значения адреса на 8.
p2++; //Увеличение значения адреса на 4.
i ++; //Увеличение значения адреса на 4.
			

К указателям так же применимы операции отношения ==, !=, <, >, <=, >=. Иными словами, указатели можно сравнивать. Например, если указывает на пятый элемент массива, а на первый, то отношение истинно. Кроме того, любой указатель всегда можно сравнить на равенство с константой нулевого указателя (NULL)1). Однако все эти утверждения верны, если речь идёт об указателях, ссылающихся на один массив. В противном случае результат арифметических операций и операций отношения будет не определён.

2.7 Стандартные функции

В C++ определены стандартные функции над арифметическими операндами1)табл. 2.8 приведены некоторые из них.

Таблица 2.8. Стандартные математические функции
ОбозначениеДействие
abs(x)Модуль целого числа
fabs(x)Модуль вещественного числа
sin(x)Синус числа
cos(x)Косинус числа
tan(x)Тангенс числа
atan(x)Арктангенс числа
acos(x)Арккосинус числа
asin(x)Арксинус числа
exp(x)Экспонента,
log(x)Натуральный логарифм,
log10(x)Десятичный логарифм,
sqrt(x)Корень квадратный,
pow(x,y)Возведение числа в степень
ceil(x)Округление числа до ближайшего большего целого
floor(x)Округление числа до ближайшего меньшего целого

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

Таблица 2.9. Примеры записи математических выражений
Математическая записьЗапись на языке С++
pow((a+b) *(a+b),1./3) или pow(pow(a+b,2),1./3)
pow(cos(x), 4)
exp(2*x)
exp(5*sin(x/2))
pow(sin(sqrt(x)),2)
log(fabs(x -2))
log(a)/log(b)
log10(x*x+1)/log10(4)
z=x*x+y*y; sin(z)+cos(z/(2*y))+sqrt(z);

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

Так, ошибка возникает, если — отрицательное число, а — дробь. Предположим, что — правильная дробь вида . Если знаменатель чётный, это означает вычисление корня чётной степени из отрицательного числа, а значит, операция не может быть выполнена. В противном случае, если знаменатель m нечётный, можно воспользоваться выражением z = –pow(fabs(x),y). Например, вычисление кубического корня из вещественного числа можно представить командой:

z=(x<0)? -pow(fabs(x),(double)1/3): pow(x,(double)1/3);
		

2.8 Структура программы

Программа на языке С++ состоит из функций, описаний и директив препроцессора.

Одна из функций должна обязательно носить имя main. Элементарное описание функции имеет вид:

тип_результата имя_функции (параметры)
{
	оператор1;
	оператор2;
	...
	операторN;
}
		

Здесь, тип_результата — это тип того значения, которое функция должна вычислить (если функция не должна возвращать значение, указывается тип void), имя_функции — имя, с которым можно обращаться к этой функции, параметры — список аргументов функции (может отсутствовать), оператор1, оператор2,..., операторN — операторы, представляющие тело функции, они обязательно заключаются в фигурные скобки и каждый оператор заканчивается точкой с запятой. Как правило, программа на С++ состоит из одной или нескольких, не вложенных друг в друга, функций.

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

#include <имя_библиотеки>
		

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

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

директивы препроцессора
описание глобальных переменных
тип_результата имя1(параметры1)
{
	описание переменных функции имя1;
	операторы1;
}
тип_результата имя2(параметры2)
{
	описание переменных функции имя2;
	операторы2;
}
............................... .
тип_результата имяN(параметрыN)
{
	описание переменных функции имяN;
	операторыN;
}
тип_результата main(параметры)
{
	описание переменных главной функции;
	операторы главной функции;
}
		

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

Локальные переменные объявляются внутри функции и доступны только в ней. Например:

int f1 ( )
{
	//В функции f1 описана другая переменная s,
	int s;
	s =6; //ей присвоено значение 6 .
}
int f2 ( )
{
	//В функции f2 определена ещё одна переменная s,
	long int s;
	s =25; //ей присвоено значение 25.
}
int main ( )
{
	//В функции main определена вещественная переменная s,
	float s;
	s =4.5; //и ей присвоено значение 4.5.
}
		

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

float s; //Определена глобальная переменная s .
int f1 ( )
{
	//В функции f1 переменной s присваивается значение 6 .
	s =6;
}
int f2 ( )
{
	//В функции f2 переменной s присваивается значение 2.1.
	s =2.1;
}
int main ( )
{
	//В главной функции переменной s присваивается значение 4.5 .
	s =4.5;
}
		

Формальные параметры функций описываются в списке параметров функции. Работа с функциями подробно описана в главе 4.

2.9 Ввод и вывод данных

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

Функция

printf(строка форматов, список выводимых переменных);
		

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

Функция

scanf(строка форматов, список адресов вводимых переменных);
		

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

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

% флаг ширина.точность модификатор тип
		

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

Таблица 2.10. Символы управления
ПараметрНазначение
Флаги
-Выравнивание числа влево. Правая сторона дополняется пробелами. По умолчанию выравнивание вправо.
+Перед числом выводится знак "+" или "-"
ПробелПеред положительным числом выводится пробел, перед отрицательным "–"
#Выводится код системы счисления: 0 — перед восьмеричным числом, 0х (0Х) перед шестнадцатеричным числом.
Ширина
nШирина поля вывода. Если n позиций недостаточно, то поле вывода расширяется до минимально необходимого. Незаполненные позиции заполняются пробелами.
OnТо же, что и n, но незаполненные позиции заполняются нулями.
Точность
ничегоТочность по умолчанию
nДля типов e, E, f выводить n знаков после десятичной точки
Модификатор
hДля d, i, o, u, x, X тип short int.
lДля d, i, o, u, x, X тип long int.
Тип
cСимвольный тип char.
dДесятичное int со знаком.
iДесятичное int со знаком.
oВосьмеричное int unsigned.
uДесятичное int unsigned.
x,XШестнадцатеричное int unsigned, при х используются символы a-f, при Х — A - F.
fЗначение со знаком вида [-]dddd.dddd.
eЗначение со знаком вида [-]d.dddde[+|-]ddd.
EЗначение со знаком вида [-]d.ddddE[+|-]ddd.
gЗначение со знаком типа e или fв зависимости от значения и точности.
GЗначение со знаком типа e или F в зависимости от значения и точности.
sСтрока символов.

Кроме того, строка форматов может содержать некоторые специальные символы, которые приведены в табл. 2.11.

Таблица 2.11. Специальные символы
СимволНазначение
\bСдвиг текущей позиции влево.
\nПеревод строки.
\rПеревод в начало строки, не переходя на новую строку
\tГоризонтальная табуляция.
\'Символ одинарной кавычки.
\"Символ двойной кавычки.
\?Символ ?

Первой строкой программы, в которой будут применяться функции вводавывода языка С, должна быть директива #include <stdio.h>. Заголовочный файл stdio.h содержит описание функций ввода-вывода.

Рассмотрим работу функций на примере следующей задачи.

Задача 2.2. Зная a, b, c — длины сторон треугольника, вычислить площадь S и периметр P этого треугольника.

Входные данные: a, b, c. Выходные данные: S, P.

Для вычисления площади применим формулу Герона: , где — полупериметр.

Далее приведены две программы для решения данной задачи и результаты их работы (рис. 2.7,рис. 2.8).

//ЗАДАЧА 2.2 Вариант первый
#include <iostream>
#include <stdio.h>
#include <math.h>
using namespace std;
int main ( )
{
	float a, b, c, S, r; //Описание переменных.
	printf ( " a = " ); //Вывод на экран символов a=.
	//В функции scanf для вычисления адреса переменной применяется операция &.
	scanf ( " % f ",&a ); //Запись в переменную а значения введённого с клавиатуры .
	printf ( " b = " ); //Вывод на экран символов b=.
	scanf ( " % f ",&b ); //Запись в переменную b значения введённого с клавиатуры.
	printf ( " c = " ); //Вывод на экран символов c=
	scanf ( " % f ",&c ); //Запись в переменную c значения введённого с клавиатуры.
	r=(a+b+c ) / 2; //Вычисление полупериметра.
	S=sqrt ( r * ( r -a ) *( r -b )*( r -c ) ); //Вычисление площади треугольника.
	printf ( " S =%5.2 f \ t ", S ); //Вывод символов S=, значения S и символа табуляции \t.
	//Спецификация %5.2f означает, что будет выведено вещественное
	//число из пяти знаков, два из которых после точки.
	printf ( " p =%5.2 f \ n ",2 *r ); //Вывод символов p=, значения выражения 2*r
	//и символа окончания строки.
	//Оператор printf("S=%5.2f \t p=%5.2f \n",S,2*r) выдаст тот же результат.
	return 0;
}
		

Результаты работы программы к задаче 2.2 (вариант 1)


Рис. 2.7.  Результаты работы программы к задаче 2.2 (вариант 1)

//ЗАДАЧА 2.2. Вариант второй
#include <iostream>
#include <stdio.h>
#include <math.h>
using namespace std;
int main ( )
{
	float a, b, c, S, r;
	printf ( " Vvedite a, b, c \ n " ); //Вывод на экран строки символов.
	scanf ( " % f % f % f ",&a,&b,& c ); //Ввод значений.
	r=(a+b+c ) / 2;
	S=sqrt ( r * ( r-a ) * ( r-b ) * ( r -c ) );
	printf ( " S =%5.2 f \ t p =%5.2 f \ n ",S, 2 * r ); //Вывод результатов.
	return 0;
}
		

Результаты работы программы к задаче 2.2 (вариант 2)


Рис. 2.8.  Результаты работы программы к задаче 2.2 (вариант 2)

2.9.1 Объектно-ориентированные средства ввода-вывода.

Описание объектов для управления вводом-выводом содержится в заголовочном файле iostream. При подключении этого файла с помощью директивы #include <iostream> в программе автоматически создаются объекты-потоки1)cin для ввода с клавиатуры и cout для вывода на экран, а также операции помещения в поток << и чтения из потока >>.

Итак, с помощью объекта cin и операции >> можно ввести значение любой переменной. Например, если переменная i описана как целочисленная, то команда cin>> i; означает, что в переменную i будет записано некое целое число, введённое с клавиатуры. Если нужно ввести несколько переменных, следует написать cin>>x>>y>>z; .

Объект cout и операция << позволяют вывести на экран значение любой переменной или текст. Текст необходимо заключать в двойные кавычки, кроме того, допустимо применение специальных символов \t и \n (табл. 2.11). Запись cout<<i; означает вывод на экран значения переменной i. А команда cout<<x<<"\t"<<y;выведет на экран значения переменных xи y, разделённые символом табуляции.

Задача 2.3. Дано трехзначное число. Записать его цифры в обратном порядке и вывести на экран новое число.

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

Пусть P=456. Вычисление остатка от деления числа P на 10 даст его последнюю цифру (количество единиц в числе P): 456 % 10 =6.

Операция деления нацело числа P на 10 позволит уменьшить количество разрядов и число станет двузначным:

456 / 10 = 45.
			

Остаток от деления полученного числа на 10 будет следующей цифрой числа P (количество десятков в числе P):

45 % 10 = 5.
			

Последнюю цифру числа P (количество сотен) можно найти так:

456 / 100 = 4.
			

Так как в задаче требовалось записать цифры числа P в обратном порядке, значит в новом числе будет 6 сотен, 5 десятков и 4 единицы:

S = 6*100 + 5*10 + 4 = 654.
			

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

#include <iostream>
using namespace std;
int main ( int argc, char *argv [ ] )
{
unsigned int P, S; //Определение целочисленных переменных без знака .
cout<<" P = "; //Вывод на экран символов P=.
cin >>P; //Ввод заданного числа P.
S=P%10*100+P/10%10*10+P/ 100; //Вычисление нового числа S .
cout<<" S = "<<S<<endl; //Вывод на экран символов S= и значения переменной S .
return 0;
}
			

Задача 2.4. Пусть целочисленная переменная i и вещественная переменная d вводятся с клавиатуры. Определить размер памяти, отведённой для хранения этих переменных и их суммы, в байтах. Вычислить, сколько памяти будет выделено для хранения строки С Новым Годом!.Вывести на экран размеры различных типов данных языка С++ в байтах.

Далее приведён текст программы.

#include <iostream>
using namespace std;
int main ( )
{
int i; //Определение целочисленной переменной .
double d; //Определение вещественной переменной .
cout<<" i = "; cin >>i; //Ввод переменной i .
cout<<" d = "; cin >>d; //Ввод переменной d .
//Размер памяти, отведённой под переменную i .
cout<<" Размер i : "<< sizeof i <<" \ n ";
//Размер памяти, отведённой под переменную d .
cout<<" Размер d : "<< sizeof d<<" \ n ";
//Размер памяти, отведённой под значение выражения i+d .
cout<<" Размер i + d : "<< sizeof ( i+d )<<" \ n ";
cout<<" Размер строки <С Новым Годом!>: ";
//Размер памяти, отведённой под строку.
cout<<sizeof " С Новым годом! "<<" \ n ";
//Вычисление размеров различных типов данных:
cout<<" Размер char : "<< sizeof ( char )<<" \ n ";
cou<<" Размер int : "<< sizeof ( int )<<" \ n ";
cout<<" Размер short int : "<< sizeof ( short int )<<" \ n ";
cout<<" Размер long int : "<< sizeof ( long int )<<" \ n ";
cout<<" Размер long long int : ";
cout<<sizeof ( long long int )<<" \ n ";
cout<<" Размер float : "<< sizeof ( float )<<" \ n ";
cout<<" Размер double : "<< sizeof ( double )<<" \ n ";
cout<<" Размер long double : "<<sizeof ( long double )<<" \ n ";
return 0;
}
			

Результаты работы программы2)

i= 23
d= 45.76
Размер i: 4
Размер d: 8
Размер i+d: 8
Размер <С Новым годом!>:26
Размер char: 1
Размер int: 4
Размер short int: 2
Размер long int: 4
Размер long long int:8
Размер float: 4
Размер double: 8
Размер long double: 12
			

2.10 Задачи для самостоятельного решения

2.10.1 Ввод-вывод данных. Операция присваивания.

Разработать программу на языке С++. Все входные и выходные данные в задачах — вещественные числа. Для ввода и вывода данных использовать функции scanf и printf.

  1. Даны катеты прямоугольного треугольника и . Найти гипотенузу и углы треугольника .
  2. Известна гипотенуза c и прилежащий угол _ прямоугольного треугольника. Найти площадь треугольника и угол .
  3. Известна диагональ квадрата . Вычислить площадь и периметр квадрата.
  4. Дан диаметр окружности . Найти длину окружности и площадь круга .
  5. Даны три числа — . Найти среднее арифметическое и среднее геометрическое заданных чисел.
  6. Даны катеты прямоугольного треугольника и . Найти гипотенузу и периметр .
  7. Дана длина окружности . Найти радиус окружности и площадь круга .
  8. Даны два ненулевых числа и . Найти сумму , разность , произведение и частное квадратов заданных чисел.
  9. Поменять местами содержимое переменных и и вывести новые значения и .
  10. Точки и заданы координатами на плоскости: . Найти длину отрезка .
  11. Заданы два катета прямоугольного треугольника и . Вычислить площадь и периметр .
  12. Даны переменные . Изменить их значения, переместив содержимое в , — в , — в , и вывести новые значения переменных .
  13. Известна диагональ ромба . Вычислить площадь и периметр .
  14. Найти значение функции и её производной при заданном значении .
  15. Даны два ненулевых числа и . Найти сумму , разность , произведение и частное модулей заданных чисел.
  16. Известны координаты вершин квадрата и . Найти площадь и периметр .
  17. Даны длины сторон прямоугольника и . Найти площадь и периметр .
  18. Известно значение периметра равностороннего треугольника. Вычислить площадь .
  19. Задан периметр квадрата . Вычислить сторону квадрата , диагональ и площадь .
  20. Дана сторона квадрата . Вычислить периметр квадрата , его площадь и длину диагонали .
  21. Три точки заданы координатами на плоскости: и . Найти длины отрезков и .
  22. Даны переменные . Изменить их значения, переместив содержимое в , — в , — в , и вывести новые значения переменных .
  23. Даны числа — . Найти их среднее арифметическое и среднее геометрическое значения.
  24. Найти значение функции и её производной при заданном значении .
  25. Точки и заданы координатами в пространстве: . Найти длину отрезка .

2.10.2 Операции целочисленной арифметики.

Разработать программу на языке С++. Все входные данные в задачах — целые числа. Для ввода и вывода данных использовать объектно-ориентированные средства ввода-вывода.

  1. Расстояние задано в сантиметрах. Найти количество полных метров в нём и остаток в сантиметрах.
  2. Масса задана в килограммах. Найти количество полных тонн в ней и остаток в килограммах.
  3. Размер файла дан в байтах. Найти количество полных килобайтов, которые занимает данный файл и остаток в байтах.
  4. Дано двузначное число. Вывести на экран количество десятков и единиц в нём.
  5. Дано двузначное число. Найти сумму его цифр.
  6. Дано двузначное число. Найти произведение его цифр.
  7. Дано двузначное число. Вывести число, полученное при перестановке цифр исходного числа.
  8. Дано трехзначное число. Определить, сколько в нём единиц, десятков и сотен.
  9. Дано трехзначное число. Найти сумму его цифр.
  10. Дано трехзначное число. Найти произведение его цифр.
  11. Дано трехзначное число. Вывести число, полученное при перестановке цифр сотен и десятков исходного числа.
  12. Дано трехзначное число. Вывести число, полученное при перестановке цифр сотен и единиц исходного числа.
  13. Дано трехзначное число. Вывести число, полученное при перестановке цифр десятков и единиц исходного числа.
  14. С начала суток прошло секунд. Найти количество полных минут, прошедших с начала суток и остаток в секундах.
  15. С начала суток прошло секунд. Найти количество полных часов, прошедших с начала суток и остаток в секундах.
  16. Дано двузначное число. Найти сумму квадратов его цифр.
  17. Дано двузначное число. Найти квадрат разности его цифр.
  18. Расстояние задано в метрах. Найти количество полных километров в нём и остаток в метрах.
  19. Масса задана в граммах. Найти количество полных килограммов в ней и остаток в граммах.
  20. Размер файла дан в килобайтах. Найти количество полных мегабайтов, которые занимает данный файл и остаток в килобайтах.
  21. Расстояние задано в дециметрах. Найти количество полных метров в нём и остаток в сантиметрах.
  22. С начала года прошло дней. Найти количество полных недель, прошедших с начала года и остаток в днях.
  23. С начала года прошло часов. Найти количество полных дней, прошедших с начала года и остаток в часах.
  24. Дано трехзначное число. Найти сумму квадратов его цифр.
  25. Дано трехзначное число. Найти квадрат суммы его цифр.

2.10.3 Встроенные математические функции

Разработать программу на языке С++. Все входные и выходные данные в задачах — вещественные числа. Для ввода и вывода данных использовать функции scanf и printf

Вычислить значение выражения при заданном значении x. Варианты заданий представлены в табл. 2.12.

Таблица 2.12. Задачи для самостоятельного решения
Выражение
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Лекция 3. Операторы управления

В этой главе описаны основные операторы языка C++: условный оператор if, оператор выбора switch, операторы цикла while, do... while и for. Изложена методика составления алгоритмов с помощью блок-схем. Приводится большое количество примеров составления программ различной сложности.

3.1 Основные конструкции алгоритма

При разработке простейших программ несложно перейти от словесного описания к написанию программы. Однако большинство реально разрабатываемых программ довольно сложные и созданию программы предшествует разработка алгоритма1).Алгоритм — это чёткое описание последовательности действий, которые необходимо выполнить, для того чтобы при соответствующих исходных данных получить требуемый результат. Одним из способов представления алгоритма является блок-схема. При составлении блок-схемы все этапы решения задачи изображаются с помощью различных геометрических фигур. Эти фигуры называют блоками и, как правило, сопровождают надписями. Последовательность выполнения этапов указывают при помощи стрелок, соединяющих эти блоки. Типичные этапы решения задачи изображаются следующими геометрическими фигурами:

Блок начала-конца алгоритма


Рис. 3.1.  Блок начала-конца алгоритма

Блок ввода-вывода данных


Рис. 3.2.  Блок ввода-вывода данных

Арифметичёский блок


Рис. 3.3.  Арифметичёский блок

Условный блок


Рис. 3.4.  Условный блок

Линейный процесс


Рис. 3.5.  Линейный процесс

Разветвляющийся процесс


Рис. 3.6.  Разветвляющийся процесс

Циклический процесс


Рис. 3.7.  Циклический процесс

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

Линейный процесс это конструкция, представляющая собой последовательное выполнение двух или более операторов (рис. 3.5). Разветвляющийся процесс задаёт выполнение одного или другого оператора в зависимости от выполнения условия (рис. 3.6). Циклический процесс задаёт многократное выполнение оператора или группы операторов (рис. 3.7).

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

Одним из важных понятий при написании программ на С(С++) является понятие составного оператора.

3.2 Составной оператор

Составной оператор — это группа операторов, отделённых друг от друга точкой с запятой, начинающихся с открывающей фигурной скобки { и заканчивающихся закрывающейся фигурной скобкой }:

{
	оператор_1;
	...
	оператор_n;
}
		

Транслятор воспринимает составной оператор как одно целое.

Рассмотрим операторы языка С++, реализующие основные конструкции алгоритма.

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

Одна из основных конструкций алгоритма — разветвляющийся процесс. Он реализован в языке С++ двумя условными операторами: if и switch Рассмотрим каждый из них.

3.3.1 Условный оператор

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

if (условие) оператор_1; else оператор_2;
			

где условие — это логическое (или целое) выражение, переменная или константа, оператор_1 и оператор_2 — любой оператор языка С(С++) .

Работает условный оператор следующим образом. Сначала вычисляется значение выражения, указанного в скобках. Если оно не равно нулю, т.е. имеет значение истина (true), выполняется оператор_1. В противном случае, когда выражение равно нулю, т.е. имеет значение ложь (false), выполняется оператор_2. Алгоритм, который реализован в условном операторе if, представлен на рис. 3.8.

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

cin>>a; cin>>b;
if ( a==b) cout<<" a равно b ";
else cout<<" a не равно b ";
			

Внимание! Не путайте знак проверки равенства == и оператор присваивания =. Например, в записи if (a=0) b=1; синтаксической ошибки нет. Операция присваивания a=0 формирует результат и его значение проверяется в качестве условия. В данном примере присваивание b=1 не будет выполнено никогда, так как переменная a всегда будет принимать значение равное нулю, то есть ложь. Верная запись: if (a==0) b=1;.

Алгоритм условного оператора if ... else


Рис. 3.8.  Алгоритм условного оператора if ... else

Алгоритм условного оператора if


Рис. 3.9.  Алгоритм условного оператора if

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

if ( условие )
{
	оператор_1;
	оператор_2;
	...
}
else
{
	оператор_3;
	оператор_4;
	...
}
			

Альтернативная ветвь else в условном операторе может отсутствовать, если в ней нет необходимости:

if ( условие ) оператор;
или
if ( условие )
{
	оператор_1;
	оператор_2;
	...
}
			

В таком "усечённом" виде условный оператор работает так: оператор (группа операторов) либо выполняется, либо пропускается, в зависимости от значения выражения, представляющего условие. Алгоритм этого условного процесса представлен на рис. 3.9.

Пример применения условного оператора без альтернативной ветви else может быть таким:

cin>>a; cin>>b;
c =0;
//Значение переменной c изменяется только при условии, что a не равно b
if ( a!=b) c=a+b;
cout<<" c = "<<c;
			

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

if (условие_1) if (условие_2) оператор_А; else оператор_Б;
			

оператор_Б относится к условию_2, а в конструкции

if (условие_1) { if (условие_2) оператор_А; }
else оператор_Б;
			

он принадлежит оператору if с условием_1.

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

Задача 3.1. Дано вещественное число . Для функции, график которой приведён на рис. 3.10, вычислить .

Графическое представление задачи 3.1


Рис. 3.10.  Графическое представление задачи 3.1

Аналитически функцию, представленную на рис. 3.10, можно записать так:

Составим словесный алгоритм решения этой задачи:

  1. Начало алгоритма.
  2. Ввод числа (аргумент функции).
  3. Если значение меньше либо равно -2, то переход к п. 4, иначе переход к п. 5.
  4. Вычисление значения функции: , переход к п. 8.
  5. Если значение больше либо равно 1, то переход к п. 6, иначе переход к п. 7.
  6. Вычисление значения функции:, переход к п. 8.
  7. Вычисление значения функции: .
  8. Вывод значений аргумента и функции .
  9. Конец алгоритма.

Блок-схема, соответствующая описанному алгоритму, представлена на рис. 3.11.

Блок-схема алгоритма решения задачи 3.1


Рис. 3.11.  Блок-схема алгоритма решения задачи 3.1

Текст программы на языке C++ будет иметь вид:

#include <iostream>
using namespace std;
int main ( )
{
	float X,Y;
	cout<<" X = "; cin >>X;
	if (X<=_2) Y=4;
	else i f (X>=1) Y=1;
	else Y=X*X;
	cout <<" Y = " <<Y<< endl;
	return 0;
}
			

Задача 3.2. Даны вещественные числа и . Определить, принадлежит ли точка с координатами () заштрихованной области (рис. 3.12).

Графическое представление задачи 3.2


Рис. 3.12.  Графическое представление задачи 3.2

Алгоритм решения задачи 3.2


Рис. 3.13.  Алгоритм решения задачи 3.2

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

Блок-схема, описывающая алгоритм решения данной задачи, представлена на рис. 3.13.

Текст программы к задаче 3.2:

#include <iostream>
using namespace std;
int main ( )
{ float X,Y;
	cout<<" X = "; cin >>X;
	cout<<" Y = "; cin >>Y;
	if (X>=-1 && X<=3 && Y>=-2 && Y<=4)
	cout <<"Точка принадлежит области"<< endl;
	else
	cout<<"Точка не принадлежит области"<<endl;
	return 0;
}
			

Задача 3.3. Даны вещественные числа и . Определить, принадлежит ли точка с координатами () заштрихованной области (рис. 3.14).

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

Графическое представление задачи 3.3


Рис. 3.14.  Графическое представление задачи 3.3

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

  1. (0, 1) - (4, 3);
  2. (4, 3) - (5, 1);
  3. (5, 1) - (0, 1).

Следовательно, уравнение первой линии:

уравнение второй линии:

и уравнение третьей линии: .

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

  1. (0, 1) - (-4, 3);
  2. (-4, 3) - (-5, 1);
  3. (-5, 1) - (0, 1);

Следовательно, уравнение первой линии:

уравнение второй линии:

и уравнение третьей линии: .

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

Далее приведён текст программы для решения задачи 3.3.

#include <iostream>
using namespace std;
int main ( )
{
	float X,Y;
	cout<<" X = "; cin >>X;
	cout<<" Y = "; cin >>Y;
	if ( (Y<=1+( float ) 1/2 *X && Y<=-2*X+11 && Y>=1) | | (Y<=1-( float ) 1/2 *X && Y<=2*X+11 && Y>=1))
		cout <<"Точка принадлежит области"<< endl;
	else
		cout<<"Точка не принадлежит области"<< endl;
	return 0;
}
			

Задача 3.4. Написать программу решения квадратного уравнения .

Исходные данные: вещественные числа и — коэффициенты квадратного уравнения.

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

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

Составим словесный алгоритм решения этой задачи.

  1. Начало алгоритма.
  2. Ввод числовых значений переменных и .
  3. Вычисление значения дискриминанта по формуле .
  4. Если , то переход к п.5, иначе переход к п.6.
  5. Вывод сообщения "Действительных корней нет" и переход к п.8.
  6. Вычисление корней и .
  7. Вывод значений и на экран.
  8. Конец алгоритма.

Блок-схема, соответствующая этому описанию, представлена на рис. 3.15.

Текст программы, которая реализует решение квадратного уравнения:

#include <iostream>
#include <math.h>
using namespace std;
int main ( )
{
	float a, b, c, d, x1, x2;
	//Ввод значений коэффициентов квадратного уравнения.
	cout<<" a = "; cin >>a;
	cout<<" b = "; cin >>b;
	cout<<" c = "; cin >>c;
	d=b*b-4*a*c; //Вычисление дискриминанта.
	if (d<0)
		//Если дискриминант отрицательный, то вывод сообщения, о том что действительных корней нет,
		cout<<"Нет действительных корней";
	else
	{
		//иначе вычисление действительных корней
		x1=( -b+sqrt (d) ) /2/a;
		x2=( -b-sqrt (d) ) /(2 * a);
		//и вывод их значений.
		cout<<" X1 = "<<x1<<" \t X2 = "<<x2<<" \n ";
	}
	return 0;
}
			

Алгоритм решения квадратного уравнения


Рис. 3.15.  Алгоритм решения квадратного уравнения

Задача 3.5. Составить программу нахождения действительных и комплексных корней квадратного уравнения .

Исходные данные: вещественные числа a, b и c — коэффициенты квадратного уравнения.

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

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

Можно выделить следующие этапы решения задачи:

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

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

На рис. 3.16 изображена блок-схема решения задачи. Блок 1 предназначен для ввода коэффициентов квадратного уравнения. В блоке 2 осуществляется вычисление дискриминанта. Блок 3 осуществляет проверку знака дискриминанта, если дискриминант отрицателен, то корни комплексные, их расчёт происходит в блоке 4 (действительная часть корня записывается в переменную , модуль мнимой — в переменную ), а вывод — в блоке 5 (первый корень , второй — ). Если дискриминант положителен, то вычисляются действительные корни уравнения (блок 6) и выводятся на экран (блок 7).

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

#include <iostream>
#include <math.h>
using namespace std;
int main ( )
{
	float a, b, c, d, x1, x2;
	cout<<" a = "; cin>>a;
	cout<<" b = "; cin>>b;
	cout<<" c = "; cin>>c;
	d=b*b-4*a*c;
	if (d<0)
	{ //Если дискриминант отрицательный, то вывод соответствующего сообщения.
		cout<<"Нет вещественных корней \n ";
		x1=-b/(2 * a ); //Вычисление действительной части комплексных корней.
		x2=sqrt ( fabs (d) ) /(2 * a ); //Вычисление модуля мнимой части комплексных корней
		//Сообщение о комплексных корнях уравнения вида ax2 + bx + c = 0.
		cout<<"Комплексные корни уравнения \n ";
		cout<<a<<" x ^2+ "<<b<<" x + "<<c<<" =0 \n ";
		//Вывод значений комплексных корней в виде x1 + ix2, x1 - ix2 
		if ( x2>=0)
		{
			cout<<x1<<" + "<<x2<<" i \t ";
			cout<<x1<<" -"<<x2<<" i \n ";
		}
		else
		{
			cout<<x1<<" -"<<abs ( x2 )<<" i \t ";
			cout<<x1<<" + "<<abs ( x2 )<<" i \n ";
		}
	}
	else
	{
		//Если дискриминант положительный, вычисление действительных корней и вывод их на экран.
		x1=( -b+sqrt (d) ) /2/a;
		x2=( -b- sqrt (d) ) /(2 * a );
		cout<<"Вещественные корни уравнения \n ";
		cout<<a<<" x ^2+ "<<b<<" x + "<<c<<" =0 \n ";
		cout<< X1 = "<<x1<<" \t X2 = "<<x2<<" \n ";
	}
	return 0;
}
			

Алгоритм решения задачи 3.5


Рис. 3.16.  Алгоритм решения задачи 3.5

Результаты работы программы к задаче 3.5 показаны ниже.

a=-5
b=-3
c=-4
Нет вещественных корней
Комплексные корни уравнения
-5x^2+-3x+-4=0
-0.3-0.842615i -0.3+0.842615i
==============================
a=2
b=-3
c=1
Вещественные корни уравнения
2x^2+-3x+1=0
X1=1 X2=0.5
			

Задача 3.6. Составить программу для решения кубического уравнения .

Кубическое уравнение имеет вид

(3.1)

После деления на a уравнение 3.1 принимает канонический вид:

(3.2)

где .

В уравнении 3.2 сделаем замену и получим приведённое уравнение:

(3.3)

где .

Число действительных корней приведённого уравнения (3.3) зависит от знака дискриминанта (табл. 3.1) .

Таблица 3.1. Количество корней кубического уравнения
ДискриминантКоличество действительных корнейКоличество комплексных корней
12
3-

Корни приведённого уравнения могут быть рассчитаны по формулам Кардано:

(3.4)

где .

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

(3.5)

где .

Таким образом, при положительном дискриминанте кубического уравнения (3.3) расчёт корней будем вести по формулам (3.4), а при отрицательном — по формулам (3.5). После расчёта корней приведённого уравнения (3.3) по формулам (3.4) или (3.5), необходимо по формулам

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

Блок-схема решения кубического уравнения представлена на рис. 3.18.

Описание блок-схемы. В блоке 1 вводятся коэффициенты кубического уравнения, в блоках 2–3 рассчитываются коэффициенты канонического и приведённого уравнений. Блок 4 предназначен для вычисления дискриминанта. В блоке 5 проверяется знак дискриминанта кубического уравнения. Если он отрицателен, то корни вычисляются по формулам 3.5 (блоки 6–7). При положительном значении дискриминанта расчёт идёт по формулам 3.4 (блок 9, 10). Блоки 8 и 11 предназначены для вывода результатов на экран.

Текст программы с комментариями приведён ниже1).

#include <iostream>
#include <math.h>
using namespace std;
#define pi 3.14159 //Определение константы
int main ( )
{
	float a, b, c, d,D, r, s, t, p, q, ro, fi, x1, x2, x3, u, v, h, g;
	//Ввод коэффициентов кубического уравнения.
	cout<<" a = "; cin >>a;
	cout<<" b = "; cin >>b;
	cout<<" c = "; cin >>c;
	cout<<" d = "; cin >>d;
	//Расчёт коэффициентов канонического уравнения по формуле 3.2
	r=b/a; s=c /a; t=d/a;
	//Вычисление коэффициентов приведённого уравнения по формуле 3.3
	p=(3*s -r * r ) / 3; q=2* r* r * r /27 - r * s/3+t;
	//Вычисление дискриминанта кубического уравнения
	D=(p /3) * ( p /3) * ( p /3) +(q /2) * ( q /2);
	if (D<0)
	{
		//Формулы 3.5
		ro=sqrt ( ( float )( -p* p* p/27) );
		fi=-q /(2 * ro );
		fi=pi/2 - atan ( fi / sqrt (1 - fi * f i ) );
		x1=2*pow( ro, ( float ) 1/3) * cos ( f i /3)- r /3;
		x2=2*pow( ro, ( float ) 1/3) * cos ( f i /3+2* pi /3)- r /3;
		x3=2*pow( ro, ( float ) 1/3) * cos ( f i /3+4* pi /3)- r /3;
		cout<<" \n x1 = "<<x1<<" \t x2 = "<<x2;
		cout<<" \t x3 = "<<x3<<" \n ";
	}
	else
	{
		//Формулы 3.4
		if ( -q/2+sqrt (D) >0) u=pow(( - q/2+sqrt (D) ),( float ) 1/3);
		else
		if ( -q/2+sqrt (D) <0) u=-pow( fabs( -q/2+sqrt (D) ),( float ) 1/3);
		else u=0;
		if (-q/2 - sqrt (D) >0) v=pow(( -q/2 - sqrt (D) ),( float ) 1/3);
		else
		if ( -q/2 - sqrt (D) <0) v=-pow( fabs( -q/2 - sqrt (D) ),( float ) 1/3);
		else v=0;
		x1=u+v-r /3; //Вычисление действительного корня кубического уравнения.
		h= -(u+v)/2 - r /3; //Вычисление действительной
		g=(u-v) /2 -sqrt (( float ) 3); //и мнимой части комплексных корней
		cout<<" \ n x1 = "<<x1;
		if (x2>=0)
		{
			cout<<x1<<" + "<<x2<<" i \t ";
			cout<<x1<<" -"<<x2<<" i \n ";
		}
		else
		{
			cout<<x1<<" -"<<fabs ( x2 )<<" i \t ";
			cout<<x1<<" + "<<fabs ( x2 )<<" i \n ";
		}
	}
	if (g>=0)
	{
		cout<<" \t x2 = "<<h<<" + "<<g<<" i ";
		cout<<" \t x3 = "<<h<<" -"<<g<<" i \n ";
	}
	else
	{
		cout<<" \t x2 = "<<h<<" -"<<fabs (g)<<" i ";
		cout<<" \t x2 = "<<h<<" + "<<fabs (g)<<" i ";
}
return 0;
}
			

Алгоритм решения кубического уравнения


Рис. 3.17.  Алгоритм решения кубического уравнения

Задача 3.7. Заданы коэффициенты и биквадратного уравнения . Найти все его действительные корни.

Входные данные: .

Выходные данные: .

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

Опишем алгоритм решения этой задачи (рис. 3.18):

  1. Ввод коэффициентов биквадратного уравнения и (блок 1).
  2. Вычисление дискриминанта уравнения (блок 2).
  3. Если d < 0 (блок 3), вывод сообщения, что корней нет (блок 4), а иначе определяются корни соответствующего квадратного уравнения y1 и y2(блок 5).
  4. Если y1 < 0 и y2 < 0 (блок 6), то вывод сообщения, что корней нет (блок 7).
  5. Если y1 >= 0 и y2 >= 0 (блок 8), то вычисляются четыре корня по формулам (блок 9) и выводятся значения корней (блок 10).
  6. Если условия 4) и 5) не выполняются, то необходимо проверить знак y1. Если y1 >= 0 (блок 11), то вычисляются два корня по формуле (блок 12), иначе (если y2 >= 0) вычисляются два корня по формуле (блок 13). Вывод вычисленных значений корней (блок 14).

Алгоритм решения биквадратного уравнения


увеличить изображение

Рис. 3.18.  Алгоритм решения биквадратного уравнения

Текст программы решения биквадратного уравнения приведён ниже.

Внимание! Если в условном операторе проверяется двойное условие, необходимо применять логические операции ||, &&, !.Например, условие "если y1 и y2 положительны" правильно записать так: if (y1>=0 && y2>=0).

#include <iostream>
#include <math.h>
using namespace std;
int main ( )
{ //Описание переменных:
	//a, b, c - коэффициенты биквадратного уравнения,
	//d - дискриминант,
	// x1, x2, x3, x4 - корни биквадратного уравнения,
	//y1, y2 - корни квадратного уравнения ay^2+by+c =0,
	float a, b, c, d, x1, x2, x3, x4, y1, y2;
	//Ввод коэффициентов уравнения.
	cout<<" a = "; cin >>a;
	cout<<" b = "; cin >>b;
	cout<<" c = "; cin >>c;
	d=b* b-4*a*c; //Вычисление дискриминанта.
	if ( d<0) //Если дискриминант отрицательный, вывод сообщения "Корней нет".
	cout<<" Нет действительных корней \ n ";
	else //Если дискриминант положительный,
	{
		//Вычисление корней соответствующего квадратного уравнения .
		y1=( -b+sqrt ( d ) ) /2/ a;
		y2=( -b- sqrt ( d ) ) /(2 * a );
		//Если оба корня квадратного уравнения отрицательные,
		if ( y1<0 && y2<0)
		//вывод сообщения "Корней нет"
		cout<<" Нет действительных корней \ n ";
		//Если оба корня квадратного уравнения положительные,
		else if ( y1>=0 && y2>=0)
		{ //Вычисление четырёх корней биквадратного уравнения
			x1=sqrt ( y1 );
			x2=-x1;
			x3=sqrt ( y2 );
			x4=- sqrt ( y2 );
			//Вывод корней уравнения на экран .
			cout<<" \t X1 = "<<x1<<" \t X2 = "<<x2;
			cout<<" \t X3 = "<<x3<<" \t X4 = "<<x4<<" \n ";
		}
		//Если не выполнились условия
		// 1.y1<0 и y2<0
		// 2.y1>=0 и y2>=0,
		//то проверяем условие y1>=0.
		else if ( y1>=0) //Если оно истинно
		{ //вычисляем два корня биквадратного уравнения.
			x1=sqrt ( y1 );
			x2=-x1;
			cout<<" X1 = "<<x1<<" \t X2 = "<<x2<<" \n ";
		}
		else
		{ //Если условие y1>=0 ложно, то вычисляем два корня биквадратного уравнения
			x1=sqrt ( y2 );
			x2=-x1;
			cout<<" X1 = "<<x1<<" \t X2 = "<<x2<<" \n ";
		}
	}
return 0;
}
			

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

3.3.2 Оператор варианта

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

switch (выражение)
{
	case значение_1: Операторы_1; break;
	case значение_2: Операторы_2; break;
	case значение_3: Операторы_3; break;
	...
	case значение_n: Операторы_n; break;
	default : Операторы; break;
}
			

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

Альтернативная ветвь default может отсутствовать, тогда оператор имеет вид:

switch (выражение)
{
	case значение_1: Операторы_1; break;
	case значение_2: Операторы_2; break;
	case значение_3: Операторы_3; break;
	...
	case значение_n: Операторы_n; break;
}
			

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

Рассмотрим применение оператора варианта.

Задача 3.8. Вывести на печать название дня недели, соответствующее заданному числу , при условии, что в месяце 31 день и 1-е число — понедельник.

Для решения задачи воспользуемся операцией %, позволяющей вычислить остаток от деления двух чисел, и условием, что 1-е число — понедельник. Если в результате остаток от деления (обозначим его ) заданного числа на семь будет равен единице, то это понедельник, двойке — вторник, тройке — среда и так далее. Следовательно, при построении алгоритма необходимо использовать семь условных операторов, как показано рис. 3.19. Решение задачи станет значительно проще, если при написании программы воспользоваться оператором варианта switch:

#include <iostream>
using namespace std;
int main ( )
{ unsigned int D,R; //Описаны целые положительные числа.
	cout<<" D = "; cin>>D; //Ввод числа от 1 до 31.
	R=D%7;
	switch (R)
	{
	case 1 : cout<<" Понедельник \n "; break;
	case 2 : cout<<" Вторник \n "; break;
	case 3 : cout<<" Среда \n "; break;
	case 4 : cout<<" Четверг \n "; break;
	case 5 : cout<<" Пятница \n "; break;
	case 6 : cout<<" Суббота \n "; break;
	case 0 : cout<<" Воскресенье \n "; break;
	}
return 0;
}
			

В предложенной записи оператора варианта отсутствует ветвь default. Это объясняется тем, что переменная может принимать только одно из указанных значений, т.е. 1, 2, 3, 4, 5, 6 или 0. Однако программа будет работать неправильно, если пользователь введёт значение , превышающее 31. Чтобы избежать подобной ошибки лучше сделать дополнительную проверку входных данных:

#include <iostream>
using namespace std;
int main ( )
{
	unsigned int D,R;
	cout<<" \ n D = "; cin >>D;
	if (D<32) //Проверка введённого значения.
	{
		R=D%7;
		switch (R)
		{
			case 1 : cout<<" Понедельник \n "; break;
			case 2 : cout<<" Вторник \n "; break;
			case 3 : cout<<" Среда \n "; break;
			case 4 : cout<<" Четверг \n "; break;
			case 5 : cout<<" Пятница \n "; break;
			case 6 : cout<<" Суббота \n "; break;
			case 0 : cout<<" Воскресенье \n "; break;
		}
	}
	//Сообщение об ошибке в случае некорректного ввода.
	else cout<<" ОШИБКА! \n ";
	return 0;
}
			

Алгоритм решения задачи 3.8


увеличить изображение

Рис. 3.19.  Алгоритм решения задачи 3.8

Задача 3.9. По заданному номеру месяца вывести на экран его название.

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

#include <iostream>
using namespace std;
int main ( )
{
	unsigned int m; //Описано целое положительное число.
	cout<<" m = "; cin >>m;
	switch (m)
	{
		//В зависимости от значения m выводится название месяца.
		case 1 : cout<<" Январь \n "; break;
		case 2 : cout<<" Февраль \n "; break;
		case 3 : cout<<" Март \n "; break;
		case 4 : cout<<" Апрель \n "; break;
		case 5 : cout<<" Май \n "; break;
		case 6 : cout<<" Июнь \n "; break;
		case 7 : cout<<" Июль \n "; break;
		case 8 : cout<<" Август \n "; break;
		case 9 : cout<<" Сентябрь \n "; break;
		case 1 0 : cout<<" Октябрь \n "; break;
		case 1 1 : cout<<" Ноябрь \n "; break;
		case 1 2 : cout<<" Декабрь \n "; break;
		//Если значение переменной m выходит за пределы области
		//допустимых значений, то выдаётся сообщение.
		default : cout<<" ОШИБКА! \n "; break;
	}
return 0;
}
			

3.4 Операторы цикла

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

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

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

3.4.1 Оператор цикла с предусловием

На рис. 3.20 изображена блок-схема алгоритма цикла с предусловием. Оператор, реализующий этот алгоритм в С++, имеет вид:

while (условие) оператор;
			

здесь условие — логическое или целочисленное выражение, оператор — любой оператор языка С(С++) .

Алгоритм циклической структуры с предусловием


Рис. 3.20.  Алгоритм циклической структуры с предусловием

Работает цикл с предусловием следующим образом. Вычисляется условие. Если оно истинно (не равно нулю), то выполняется оператор, и условие проверяется вновь. В противном случае цикл заканчивается, и управление передаётся оператору, следующему за телом цикла. Условие вычисляется перед каждой итерацией цикла. Если при первой проверке выражение равно нулю, цикл не выполнится ни разу. Тип выражения должен быть арифметическим или приводимым к нему.

Если тело цикла состоит более чем из одного оператора, необходимо использовать составной оператор:

while (условие)
{
	оператор 1;
	оператор 2;
	...
	оператор n;
}
			

Рассмотрим пример. Пусть необходимо вывести на экран таблицу значений функции на отрезке [0; π] с шагом 0.1. Применив цикл с предусловием, получим:

#include <stdio.h>
#include <math.h>
#define PI 3.14159
using namespace std;
int main ( )
{
	float x, y; //Описание переменных
	x=0; //Присваивание параметру цикла стартового значения
	//Цикл с предусловием
	while (x<=PI ) //Пока параметр цикла не превышает конечное значение
	{ //выполнять тело цикла
	y=exp ( sin ( x ) ) * cos ( x ); //Вычислить значение y
	//Вывод на экран пары x и y .
	printf ( " \t x =%5.2 f \t y =%5.4 f \n ",x, y );
	x+=0.1; //Изменение параметра цикла
	// (переход к следующему значению x )
	} //Конец цикла
	return 0;
}
			

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

x= 1.00	y=1.2534	x= 2.10	y=-1.1969
x= 1.10	y=1.1059	x= 2.20 y=-1.3209
x= 1.20	y=0.9203	x= 2.30 y=-1.4045
x= 1.30	y=0.7011	x= 2.40 y=-1.4489
x= 1.40	y=0.4553	x= 2.50 y=-1.4576
x= 1.50 y=0.1918	x= 2.60 y=-1.4348
x= 1.60 y=-0.0793	x= 2.70 y=-1.3862
x= 1.70 y=-0.3473	x= 2.80 y=-1.3172
x= 1.80 y=-0.6017	x= 2.90 y=-1.2334
x= 1.90 y=-0.8328	x= 3.00 y=-1.1400
x= 2.00 y=-1.0331	x= 3.10 y=-1.0416
			
Пример 1.1. (html, txt)

3.4.2 Оператор цикла с постусловием

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

Алгоритм циклической структуры с постусловием


Рис. 3.21.  Алгоритм циклической структуры с постусловием

В С++ цикл с постусловием реализован конструкцией

do оператор while (условие);
			

здесь условие — логическое или целочисленное выражение, оператор — любой оператор языка С(С++). Если тело цикла состоит более чем из одного оператора:

do
{
	оператор_1;
	оператор_2;
	...
	оператор_n;
}
while (условие);
			

Работает цикл следующим образом. В начале выполняется оператор, представляющий собой тело цикла. Затем вычисляется условие. Если оно истинно (не равно нулю), оператор тела цикла выполняется ещё раз. В противном случае цикл завершается, и управление передаётся оператору, следующему за циклом.

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

Если применить цикл с постусловием для создания программы, которая выводит таблицу значений функции на отрезке [0; π] с шагом 0.1, получим:

#include <iostream>
#include <stdio.h>
#include <math.h>
#define PI 3.14159
using namespace std;
int main ( )
{
	float x, y; //Описание переменных
	x=0; //Присваивание параметру цикла стартового значения
	do //Цикл с постусловием
	{ //Выполнять тело цикла
	y=exp ( sin ( x ) ) * cos ( x );
	printf ( " \t x =%5.2 f \t y =%5.4 f \n ",x, y );
	x+=0.1; //Изменение параметра цикла
}
while (x<=PI ); //пока параметр цикла не превышает конечное значение
return 0;
}
			

Результаты работы этой программы будут такими.

3.4.3 Оператор цикла for с параметром

Кроме того, в С++ предусмотрен цикл for с параметром:

for (начальные_присваивания;условие;последействие)
оператор;
			

где начальные_присваивания — оператор или группа операторов, разделённых запятой1), применяются для присвоения начальных значений величинам, используемым в цикле, в том числе параметру цикла, и выполняются один раз в начале цикла; условие — целое или логическое выражение, которое определяет условие входа в цикл, если условие истинно (не равно нулю), то цикл выполняется; последействие — оператор или группа операторов, разделённых запятой, которые выполняются после каждой итерации и служат для изменения параметра цикла; оператор — любой оператор языка, представляющий собой тело цикла. Последействие или оператор должны влиять на условие, иначе цикл никогда не закончится. Начальные_присваивания, выражение или последействие в записи оператора for могут отсутствовать, но при этом "точки с запятой" должны оставаться на своих местах. Опишем алгоритм работы цикла for:

  1. Выполняются начальные_присваивания.
  2. Вычисляется условие, если оно не равно 0 (true), то выполняется переход к п.3. В противном случае выполнение цикла завершается.
  3. Выполняется оператор.
  4. Выполняется оператор последействие и осуществляется переход к п.2, опять вычисляется значение выражения и т.д.

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

Алгоритм работы цикла с параметром


Рис. 3.22.  Алгоритм работы цикла с параметром

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

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

for (начальные_присваивания; условие; последействие)
{
	оператор_1;
	...
	оператор_n;
}
			

Применение цикла for рассмотрим на примере печати таблицы значений функции на отрезке [0;π] с шагом 0.1:

#include <stdio.h>
#include <math.h>
#define PI 3.14159
using namespace std;
int main ( )
{
	float x, y;
	//Параметру цикла присваивается начальное значение, если оно не превышает конечное значение,
	//то выполняются операторы тела цикла и значение параметра изменяется, в противном случае
	//цикл заканчивается.
	for ( x=0;x<=PI; x+=0.1)
	{
	y=exp ( sin ( x ) ) * cos ( x );
	printf ( " \t x =%5.2 f \t y =%5.4 f \n ", x, y );
	}
return 0;
}
			

Блок-схема цикла с параметром


Рис. 3.23.  Блок-схема цикла с параметром

Программный код выдаст результат.

3.4.4 Операторы передачи управления

Операторы передачи управления принудительно изменяют порядок выполнения команд. В С++ таких операторов четыре: goto, break, continue и return.

Оператор goto метка, где метка обычный идентификатор, применяют для безусловного перехода, он передаёт управление оператору с меткой: метка: оператор;2).

Оператор break осуществляет немедленный выход из циклов while, do... while и for, а так же из оператора выбора switch. Управление передаётся оператору, находящемуся непосредственно за циклом или оператором выбора.

Оператор continue начинает новую итерацию цикла, даже если предыдущая не была завершена.

Оператор return выражение завершает выполнение функции и передаёт управление в точку её вызова. Если функция возвращает значение типа void, то выражение в записи оператора отсутствует. В противном случае выражение должно иметь скалярный тип.

3.5 Решение задач с использованием циклов

Рассмотрим использование циклических операторов на конкретных примерах.

Задача 3.10. Написать программу решения квадратного уравнения . Предусмотреть проверку ввода данных.

Решение квадратного уравнения было подробно рассмотрено в задаче 3.4. Однако алгоритм, изображённый на рис. 3.15, не будет работать, если пользователь введёт нулевое значение в переменную a (при попытке вычислить корни уравнения произойдёт деление на ноль). Чтобы избежать подобной ошибки нужно в программе предусмотреть проверку входных данных, например, так как показано на рис. 3.24. Вводится значение переменной a, если оно равно нулю, то ввод повторяется, иначе следует алгоритм вычисления корней квадратного уравнения. Здесь применяется цикл с постусловием, так как значение переменной необходимо ввести, а затем проверить его на равенство нулю.

Блок-схема проверки ввода данных


Рис. 3.24.  Блок-схема проверки ввода данных

Программа решения задачи:

#include <iostream>
#include <math.h>
using namespace std;
int main ( )
{
	float a, b, c, d, x1, x2;
	//Проверка ввода значения коэффициента a .
	do //Выполнять тело цикла пока а равно нулю
	{
	cout<<" a = "; cin >>a;
	}
	while ( a==0);
	cout<<" b = "; cin >>b;
	cout<<" c = "; cin >>c;
	d=b* b-4*a*c;
	if (d<0) cout<<" Нет вещественных корней";
	else
	{
	x1=( -b+sqrt (d) ) /2/a;
	x2=( -b- sqrt (d) ) /(2 *a );
	cout<<" X1 = "<<x1<<" \t X2 = "<<x2<<" \n ";
	}
	return 0;
}
		

Задача 3.11. Найти наибольший общий делитель (НОД) натуральных чисел и .

Входные данные: и .

Выходные данные: — НОД.

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

Таблица 3.2. Поиск НОД для чисел A = 25 и B = 15.
ШагAB
Исходные данные2515
Шаг 11015
Шаг 2105
Шаг 3, НОД55

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

#include <iostream>
using namespace std;
int main ( )
{
	unsigned int a, b;
	cout<<" A = "; cin>>a;
	cout<<" B = "; cin>>b;
	//Если числа не равны, выполнять тело цикла
	while ( a!=b)
	//Если число A больше, чем B, то уменьшить его значение на B,
	if ( a>b) a=a-b;
	//иначе уменьшить значение числа B на A
	else b=b-a;
	cout<<" НОД= "<<a<<" \n ";
	return 0;
}
		

Результат работы программы не изменится, если для её решения воспользоваться циклом с постусловием do...while:

#include <iostream>
using namespace std;
int main ( )
{
	unsigned int a, b;
	cout<<" A = "; cin >>a;
	cout<<" B = "; cin >>b;
	do
	if ( a>b ) a=a-b; else b=b-a;
	while ( a !=b );
	cout<<" НОД= "<<a<<" \n ";
	return 0;
}
		

Поиск наибольшего общего делителя двух чисел.


Рис. 3.25.  Поиск наибольшего общего делителя двух чисел.

Задача 3.12. Вычислить факториал числа .

Входные данные: — целое число, факториал которого необходимо вычислить.

Выходные данные: factorial — целое число, значение факториала числа , произведение чисел от 1 до .

Промежуточные переменные: — параметр цикла, целочисленная переменная, последовательно принимающая значения 2, 3, 4 и так далее до .

Блок-схема приведена на рис. 3.26.

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

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


Рис. 3.26.  Алгоритм вычисления факториала.

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

#include <iostream>
using namespace std;
int main ( )
{
	unsigned long long int factorial; 1
	unsigned int N, i;
	for ( cout<<" N = ", cin >>N, factorial =1, i =2; i<=N; factorial*=i, i ++);
	cout<<" факториал= "<<factorial <<" \ n ";
	return 0;
}
		

Задача 3.13. Вычислить сумму натуральных чётных чисел, не превышающих .

Входные данные: — целое число.

Выходные данные: — сумма чётных чисел.

Промежуточные переменные: — параметр цикла, принимает значения 2, 4, 6, 8 и так далее, также имеет целочисленное значение.

При сложении нескольких чисел необходимо накапливать результат в определённом участке памяти (), каждый раз считывая из этого участка () предыдущее значение суммы () и прибавляя к нему слагаемое . Для выполнения первого оператора накапливания суммы из участка памяти необходимо взять такое число, которое не влияло бы на результат сложения. Перед началом цикла переменной, предназначенной для накапливания сумы, необходимо присвоить значение нуль. Блок-схема решения этой задачи представлена на рис. 3.27.

Решим задачу двумя способами: с применением циклов while и for:

//Решение задачи с помощью цикла while
#include <iostream>
using namespace std;
int main ( )
{
	unsigned int N, i, S;
	cout<<" N = "; cin >>N;
	S=0;
	i =2;
	while ( i<=N)
	{
	S=S+i;
	i=i +2;
	}
	cout<<" S = "<<S<<" \n ";
	return 0;
	}
	//__________________________________
	//Решение задачи с помощью цикла for
#include <iostream>
using namespace std;
int main ( )
	{
	unsigned int N, i, S;
	for ( cout<<" N = ", cin >>N, S=0, i =2; i<=N; S+=i, i +=2);
	cout<<" S = "<<S<<" \n ";
	return 0;
}
		

Алгоритм вычисления суммы чётных натуральных чисел.


Рис. 3.27.  Алгоритм вычисления суммы чётных натуральных чисел.

Задача 3.14. Дано натуральное число . Определить — количество делителей этого числа, меньших самого числа (Например, для делители 1, 2, 3, 4, 6. Количество ).

Входные данные: — целое число.

Выходные данные: целое число — количество делителей .

Промежуточные переменные: — параметр цикла, возможные делители числа .

В блок-схеме, изображённой на рис. 3.28, реализован следующий алгоритм: в переменную , предназначенную для подсчёта количества делителей заданного числа, помещается значение, которое не влияло бы на результат, т.е. нуль. Далее организовывается цикл, в котором изменяющийся параметр выполняет роль возможных делителей числа . Если заданное число делится нацело на параметр цикла , это означает, что является делителем , и значение переменной следует увеличить на единицу. Цикл необходимо повторить раз.

Алгоритм определения делителей натурального числа.


Рис. 3.28.  Алгоритм определения делителей натурального числа.

Текст программы на С++:

#include <iostream>
using namespace std;
int main ( )
{
	unsigned int N, i,K;
	cout<<" N = "; cin >>N;
	for (K=0, i =1; i<=N/ 2; i ++) if (N%i ==0) K++;
	cout<<" K = "<<K<<" \n ";
	return 0;
}
		

Задача 3.15. Дано натуральное число . Определить, является ли оно простым. Натуральное число называется простым, если оно делится без остатка только на единицу и на само себя. Число 13 — простое, так как делится только на 1 и 13, а число 12 таковым не является, так как делится на 1, 2, 3, 4, 6 и 12.

Входные данные: — целое число.

Выходные данные: сообщение.

Промежуточные переменные: — параметр цикла, возможные делители числа .

Необходимо проверить, есть ли делители числа в диапазоне от 2 до (рис. 3.29). Если делителей нет, — простое число, иначе оно таковым не является. Обратите внимание на то, что в алгоритме предусмотрено два выхода из цикла. Первый — естественный, при исчерпании всех значений параметра, а второй — досрочный. Нет смысла продолжать цикл, если будет найден хотя бы один делитель из указанной области изменения параметра.

Алгоритм определения простого числа.


Рис. 3.29.  Алгоритм определения простого числа.

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

#include <iostream>
using namespace s t d;
int main ( )
{
	unsigned int N, i;
	bool Pr;
	cout<<" N = "; cin >>N;
	Pr=true; //Предположим, что число простое
	for ( i =2; i <=N/ 2; i ++)
	if (N%i ==0) //Если найдётся хотя бы один делитель, то
	{
		Pr=false; //число простым не является и
		break; //досрочный выход из цикла
	}
	if ( Pr ) //Проверка значения логического параметра и вывод на печать
		//соответствующего сообщения
		cout<<N<<" - простое число\n ";
	else
		cout<<N<<" - не является простым\n ";
	return 0;
}
		

Задача 3.16. Дано натуральное число . Определить количество цифр в числе.

Входные данные: — целое число.

Выходные данные: — количество цифр в числе.

Промежуточные данные: — переменная для временного хранения значения 1).

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

Таблица 3.3. Определение количества цифр числа
kolN
112345
212345 / 10 = 1234
31234 / 10 = 123
4123 / 10 = 12
512 / 10 = 1
1 / 10 = 0

Алгоритм определения количества цифр в числе представлен на рис. 3.30.

#include <iostream>
using namespace std;
int main ( )
{
	unsigned long int N, M;
	unsigned int kol;
	cout<<" N = "; cin >>N;
	for (M=N, kol =1; M/10 >0; kol ++,M/=10);
	cout<<" kol = "<<kol <<endl;
	return 0;
}
		

Алгоритм определения количества цифр в числе.


Рис. 3.30.  Алгоритм определения количества цифр в числе.

Задача 3.17. Дано натуральное число . Определить, содержит ли это число нули и в каких разрядах они расположены (например, число 11011110111 содержит ноль в третьем и восьмом разрядах, а число 120405 — в первом и третьем).

Входные данные: — целое число.

Выходные данные: — позиция цифры в числе.

Промежуточные данные: — параметр цикла, — переменная для временного хранения значения .

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

Таблица 3.4. Определение текущей цифры числа
iЧисло МЦифраНомер позиции
1120405120405 % 10 = 50
212040/10 = 120412040 % 10 = 01
31204/10 = 1201204 % 10 = 42
4120/10 = 12120 % 10 = 03
512/10 = 112 % 10 = 24
61/10 = 01 % 10 = 15

Программный код к задаче 3.17.

#include <iostream>
using namespace s t d;
int main ( )
{
	unsigned long int N,M; int kol, i;
	cout<<" N = "; cin >>N;
	for ( kol =1,M=N;M/10 >0; k o l ++,M/=10);
	for (M=N, i =0; i <kol;M/=10, i ++)
	if (M%10==0) cout<<"Позиция = "<<i <<endl;
	return 0;
}
		

Задача 3.18. Дано натуральное число . Получить новое число, записав цифры числа в обратном порядке. Например, 17852 — 25871.

Входные данные: — целое число.

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

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

Рассмотрим пример. Пусть , тогда .

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

#include <iostream>
using namespace std;
int main ( )
{ unsigned long int N,M, R, S; int kol, i;
	cout<<" N = "; cin >>N;
	for (R=1, kol =1,M=N;M/10 >0; kol ++,R* =10,M/=10);
	for ( S=0,M=N, i =1; i<=kol; S+=M%10*R,M/=10,R/=10, i ++);
	cout<<" S = "<<S<<endl;
	return 0;
}
		

Задача 3.19. Проверить, является ли заданное число палиндромом3). Например, числа 404, 1221 — палиндромы.

Входные данные: — целое число.

Выходные данные: сообщение.

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

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

Текст программы на языке С++:

#include <iostream>
using namespace std;
int main ( )
{ unsigned long int N,M,R, S;
	int kol, i;
	cout<<" N = "; cin>>N;
	for (R=1, kol =1,M=N;M/10 >0; kol++,R* =10,M/=10);
	for (S=0,M=N, i =1; i<=kol; S+=M%10*R,M/=10,R/=10, i++);
	if (N==S) cout<<"Число - палинром"<<endl;
	else cout<<"Число не является палиндромом"<<endl;
	return 0;
}
		

Задача 3.20. Поступает последовательность из вещественных чисел. Определить наибольший элемент последовательности.

Входные данные: — целое число; — вещественное число, определяет текущий элемент последовательности.

Выходные данные: — вещественное число, элемент последовательности с наибольшим значением.

Промежуточные переменные: — параметр цикла, номер вводимого элемента последовательности.

Алгоритм поиска наибольшего элемента в последовательности следующий (рис. 3.31). Вводится — количество элементов последовательности и — первый элемент последовательности. В памяти компьютера отводится ячейка, например с именем , в которой будет храниться наибольший элемент последовательности — максимум. Далее предполагаем, что первый элемент последовательности наибольший и записываем его в . Затем вводим второй элемент последовательности и сравниваем его с предполагаемым максимумом. Если окажется, что второй элемент больше, его записывают в ячейку . В противном случае никаких действий не предпринимаем. Потом переходим к вводу следующего элемента последовательности (), и алгоритм повторяется с начала. В результате в ячейке сохранится элемент последовательности с наибольшим значением1).

Алгоритм поиска наибольшего числа в последовательности.


Рис. 3.31.  Алгоритм поиска наибольшего числа в последовательности.

Текст программы на С++:

#include <iostream>
using namespace std;
int main ( )
{
	unsigned int i,N;
	float X,Max;
	cout<<" N = "; cin>>N;
	cout<<" X = "; cin>>X; //Ввод первого элемента последовательности
	//Параметр цикла принимает стартовое значение i =2, т.к. первый элемент
	//уже введён предположим, что он максимальный, т.е. Max=X.
	for ( i =2, Max=X; i<=N; i++)
	{
	cout<<" X = "; cin>>X; //Ввод следующих элементов последовательности.
	//Если найдётся элемент, превышающий максимум, записать его в ячейку Max,
	//теперь он предполагаемый максимум.
	if (X>Max) Max=X;
	}
	//Вывод наибольшего элемента последовательности.
	cout<<" Max = "<<Max<<" \n ";
	return 0;
}
		

Задача 3.21. Вводится последовательность целых чисел, 0 — конец последовательности. Найти наименьшее число среди положительных, если таких значений несколько2), определить, сколько их.

Блок-схема решения задачи приведена на рис. 3.32.

Алгоритм поиска минимального положительного числа в последовательности.


Рис. 3.32.  Алгоритм поиска минимального положительного числа в последовательности.

Далее приведён текст подпрограммы с подробными комментариями3).

#include <iostream>
using namespace std;
int main ( )
{
	float N, Min; int K;
	//Предположим, что в последовательности нет положительных чисел, K=0.
	//Вводим число и если оно не равно нулю
	for ( cout<<" N = ", cin>>N,K=0;N!=0; cout<<" N = ", cin>>N)
	//проверяем является ли оно положительным.
	if (N>0)
	//если K=0, поступил 1-й положительный элемент, предположим, что он минимальный.
	if (K==0) {K=1;Min=N; }
	//если элемент не первый, сравниваем его с предполагаемым минимумом,
	//если элемент меньше, записываем его в Min и сбрасываем счётчик
	else if (N<Min) {Min=N;K=1;}
	//если элемент равен минимуму, увеличиваем значение счётчика.
	else if (N==Min) K++; //Конец цикла
	//Если значение счётчика не равно нулю, печатаем значение
	//минимального элемента и количество таких элементов.
	if (K!=0) cout<<" Min = "<<Min<<" \n "<<" K = "<<K<<" \n ";
	//в противном случае выдаём сообщаем.
	else cout<<"В последовательности нет положительных элементов \n ";
	return 0;
}
		

Задача 3.22. Определить, сколько раз последовательность из произвольных чисел меняет знак.

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

Пусть в переменной хранится текущий элемент последовательности, в — предыдущий. Введём первое число (до цикла) и второе (в цикле). Если их произведение отрицательно, то увеличиваем количество смен знака на 1 (k++). После чего сохраняем значение в переменную и повторяем цикл (рис. 3.33).

Алгоритм решения задачи 3.22.


Рис. 3.33.  Алгоритм решения задачи 3.22.

Предлагаем читателю самостоятельно разобраться с текстом программы на С++:

#include <iostream>
using namespace std;
int main ( )
{
	float A,B; int i,K,N;
	cout<<" N = "; cin>>N;
	for (K=0, cout<<" A = ", cin>>A, i =2; i<=N; i++)
	{
	cout<<" B = "; cin>>B;
	if (A_B<0) K++;
	A=B;
	}
	cout<<" K = "<<K<<" \n ";
	return 0;
}
		

Задача 3.23. Поступает последовательность из вещественных чисел. Определить количество простых чисел в последовательности.

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

Алгоритм поиска простых чисел в последовательности.


Рис. 3.34.  Алгоритм поиска простых чисел в последовательности.

#include <iostream>
using namespace std;
int main ( )
{
	unsigned long int X;
	unsigned int N;
	int i, k, j;
	bool Pr;
	for ( k=0, cout<<" N = ", cin >>N, i =1; i<=N; i ++)
	{
	for ( cout<<" X = ", cin >>X, Pr=true, j =2; j<=X/ 2; j ++)
	if (X%j ==0)
	{
	Pr=false;
	break;
	}
	if ( Pr ) k++;
	}
	if ( k==0) cout<<"Простых чисел нет \n ";
	else cout<<"Количество простых чисел k = "<<k<<" \n ";
	return 0;
}
		

Задача 3.24. Дано наборов ненулевых целых чисел. Каждый набор содержит не менее двух элементов, признаком его завершения является число 0. Найти количество наборов, элементы которых возрастают.

Блок-схема алгоритма решения задачи показана на рис. 3.35. Нетрудно заметить, что алгоритм реализован с помощью двух циклических процессов. Внутренний цикл проверяет является ли последовательность возрастающей, а внешний повторяет алгоритм для новой последовательности.

Алгоритм решения задачи 3.24.


Рис. 3.35.  Алгоритм решения задачи 3.24.

Программный код решения задачи 3.24:

#include <iostream>
using namespace std;
int main ( )
{
	unsigned int K, i, kol, A, B; bool pr;
	for ( cout<< K = ", cin >>K, kol =0, i =1; i<=K; i ++)
	{
	for ( pr=true, cout<<" A = ", cin >>A;A! = 0; A=B)
	{
	cout<<" B = "; cin >>B;
	if (B!=0 && A>=B) pr=false;
	}
	if ( pr ) kol++;
	}
	cout << " kol = " << kol <<endl;
	return 0;
}
		

3.6 Задачи для самостоятельного решения

3.6.1 Разветвляющийся процесс. Вычисление значения функции.

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

Задание 1


Рис. 3.36.  Задание 1

Задание 2


Рис. 3.37.  Задание 2

Задание 3


Рис. 3.38.  Задание 3

Задание4


Рис. 3.39.  Задание4

Задание 5


Рис. 3.40.  Задание 5

Задание 6


Рис. 3.41.  Задание 6

Задание 7


Рис. 3.42.  Задание 7

Задание 8


Рис. 3.43.  Задание 8

Задание 9


Рис. 3.44.  Задание 9

Задание 10


Рис. 3.45.  Задание 10

Задание 11


Рис. 3.46.  Задание 11

Задание 12


Рис. 3.47.  Задание 12

Задание 13


Рис. 3.48.  Задание 13

Задание 14


Рис. 3.49.  Задание 14

Задание 15


Рис. 3.50.  Задание 15

Задание 16


Рис. 3.51.  Задание 16

Задание 17


Рис. 3.52.  Задание 17

Задание 18


Рис. 3.53.  Задание 18

Задание 19


Рис. 3.54.  Задание 19

Задание 20


Рис. 3.55.  Задание 20

Задание 21


Рис. 3.56.  Задание 21

Задание 22


Рис. 3.57.  Задание 22

Задание 23


Рис. 3.58.  Задание 23

Задание 24


Рис. 3.59.  Задание 24

Задание 25


Рис. 3.60.  Задание 25

3.6.2 Разветвляющийся процесс. Попадание точки в область на плоскости

Разработать программу на языке С++. Даны вещественные числа и . Определить, принадлежит ли точка с координатами () заштрихованной области. Варианты заданий представлены на рисунках ниже.

Задание 1


Рис. 3.61.  Задание 1

Задание 2


Рис. 3.62.  Задание 2

Задание 3


Рис. 3.63.  Задание 3

Задание 4


Рис. 3.64.  Задание 4

Задание 5


Рис. 3.65.  Задание 5

Задание 6


Рис. 3.66.  Задание 6

Задание 7


Рис. 3.67.  Задание 7

Задание 8


Рис. 3.68.  Задание 8

Задание 9


Рис. 3.69.  Задание 9

Задание 10


Рис. 3.70.  Задание 10

Задание 11


Рис. 3.71.  Задание 11

Задание 12


Рис. 3.72.  Задание 12

Задание 13


Рис. 3.73.  Задание 13

Задание 14


Рис. 3.74.  Задание 14

Задание 15


Рис. 3.75.  Задание 15

Задание 16


Рис. 3.76.  Задание 16

Задание 17


Рис. 3.77.  Задание 17

Задание 18


Рис. 3.78.  Задание 18

Задание 19


Рис. 3.79.  Задание 19

Задание 20


Рис. 3.80.  Задание 20

Задание 21


Рис. 3.81.  Задание 21

Задание 22


Рис. 3.82.  Задание 22

Задание 23


Рис. 3.83.  Задание 23

Задание 24


Рис. 3.84.  Задание 24

Задание 25


Рис. 3.85.  Задание 25

3.6.3 Разветвляющийся процесс. Пересечение линий и решение уравнений.

Разработать программу на языке С++ для следующих заданий:

  1. Задан круг с центром в точке , радиусом и точка . Определить, находится ли точка внутри круга.
  2. Задана окружность с центром в точке и радиусом . Определить, пересекается ли заданная окружность с осью абсцисс, если пересекается — найти точки пересечения.
  3. Задана окружность с центром в точке и радиусом . Определить, пересекается ли заданная окружность с осью ординат, если пересекается — найти точки пересечения.
  4. Задана окружность с центром в точке и радиусом и прямая . Определить, пересекаются ли прямая и окружность. Если пересекаются, найти точки пересечения.
  5. Заданы окружности. Первая с центром в точке и радиусом , вторая с центром в точке и радиусом . Определить, пересекаются окружности, касаются или не пересекаются.
  6. Заданы три точки . Определить, какая из точек наиболее удалена от начала координат.
  7. Заданы три точки ). Определить, какая из точек или наименее удалена от точки .
  8. Определить, пересекаются ли линии и . Если пересекаются, найти точку пересечения.
  9. Определить, пересекает ли линия ось абсцисс. Если пересекает, найти точку пересечения.
  10. Определить, пересекаются ли линии и . Если пересекаются, найти точки пересечения.
  11. Определить, пересекаются ли линии и . Если пересекаются, найти точки пересечения.
  12. Определить, пересекаются ли линии и . Если пересекаются, найти точки пересечения.
  13. Определить, пересекаются ли линии и .Если пересекаются, найти точку пересечения.
  14. Определить, пересекает ли линия ось абсцисс. Если пересекает, найти точку пересечения.
  15. Определить, пересекаются ли параболы и . Если пересекаются, то найти точки пересечения.
  16. Определить, пересекаются ли линии и . Если пересекаются, найти точки пересечения.
  17. Найти точки пересечения линии с осью абсцисс. Если линии не пересекаются выдать соответствующее сообщение.
  18. Определить, пересекаются ли линии и . Если пересекаются, найти точки пересечения.
  19. Определить, пересекаются ли линии и . Если пересекаются, найти точки пересечения.
  20. Определить, пересекает ли линия ось абсцисс. Если пересекает, найти точки пересечения.
  21. Найти комплексные корни уравнения . Если в уравнении нет комплексных корней, вывести соответствующее сообщение.
  22. Найти комплексные корни уравнения . Если в уравнении нет комплексных корней, вывести соответствующее сообщение.
  23. Найти комплексные корни уравнения . Если в уравнении нет комплексных корней, вывести соответствующее сообщение.
  24. Заданы точки . Определить, какая из точек наименее удалена от начала координат.
  25. Даны координаты точки, не лежащей на координатных осях и . Определить номер координатной четверти, в которой находится данная точка.

3.6.4 Циклический процесс. Вычисление значений функции

Разработать программу на языке С++. Для решения задачи использовать операторы for, while, do. Варианты заданий:

  1. Вывести на экран таблицу значений функции синус в диапазоне от до с шагом .
  2. Вывести на экран таблицу квадратов первых десяти целых положительных чисел.
  3. Вывести на экран таблицу значений функции косинус в диапазоне от до с шагом .
  4. Вывести на экран таблицу кубов первых десяти целых положительных чисел.
  5. Вывести на экран таблицу значений квадратов синусов в диапазоне от до с шагом .
  6. Вывести на экран таблицу значений квадратов косинусов в диапазоне от 0 до с шагом .
  7. Вывести на экран таблицу квадратов первых десяти целых чётных положительных чисел.
  8. Вывести на экран таблицу квадратов первых десяти целых нечётных положительных чисел.
  9. Вывести на экран таблицу значений удвоенных синусов в диапазоне от до с шагом . Значения и вводятся с клавиатуры.
  10. Вывести на экран таблицу значений удвоенных косинусов в диапазоне от до с шагом . Значения и вводятся с клавиатуры.
  11. Вывести на экран таблицу кубов первых десяти целых нечётных положительных чисел.
  12. Вывести на экран таблицу кубов первых десяти целых чётных положительных чисел.
  13. Вывести на экран таблицу значений функции в диапазоне от до шагом . Значения и вводятся с клавиатуры.
  14. Вывести на экран таблицу значений функции в диапазоне от до с шагом . Значения и вводятся с клавиатуры.
  15. Вывести на экран таблицу квадратов первых десяти целых отрицательных чисел.
  16. Вывести на экран таблицу кубов первых десяти целых отрицательных чисел.
  17. Вывести на экран таблицу квадратных корней первых десяти целых положительных чисел.
  18. Вывести на экран таблицу кубических корней первых десяти целых положительных чисел.
  19. Вывести на экран таблицу значений функции в диапазоне от до с шагом . Значения и вводятся с клавиатуры.
  20. Вывести на экран таблицу значений функции в диапазоне от до с шагом . Значения и вводятся с клавиатуры.
  21. Вывести на экран таблицу квадратных корней первых десяти целых положительных чётных чисел.
  22. Вывести на экран таблицу квадратных корней первых десяти целых положительных нечётных чисел.
  23. Вывести на экран таблицу значений функции в диапазоне от -3 до 4 с шагом .
  24. Вывести на экран таблицу значений функции y в диапазоне от -2 до 2 с шагом .
  25. Вывести на экран таблицу степеней двойки в диапазоне от 0 до 10 с шагом 1.

3.6.5 Циклический процесс. Последовательности натуральных чисел

Разработать программу на языке С++ для следующих заданий:

  1. Дано целое положительное число . Вычислить сумму натуральных нечётных чисел не превышающих это число.
  2. Дано целое положительное число . Вычислить произведение натуральных чётных чисел не превышающих это число.
  3. Дано целое положительное число . Вычислить количество натуральных чисел кратных трём и не превышающих число .
  4. Задано целое положительное число . Определить значение выражения: .
  5. Вычислить количество натуральных двузначных чётных чисел не делящихся на 10.
  6. Задано целое положительное число . Определить значение выражения: .
  7. Вычислить сумму натуральных удвоенных чисел не превышающих 25.
  8. Задано целое положительное число . Определить значение выражения: .
  9. Дано целое положительное число . Вычислить сумму квадратов натуральных чётных чисел не превышающих это число.
  10. Дано целое положительное число . Вычислить количество натуральных чисел кратных пяти и не превышающих число .
  11. Определить значение выражения: .
  12. Дано целое положительное число . Вычислить сумму удвоенных натуральных нечётных чисел не превышающих это число.
  13. Задано целое положительное число . Определить значение выражения: .
  14. Найти сумму нечётных степеней двойки. Значение степени изменяется от 1 до 9.
  15. Задано целое положительное число . Определить значение выражения: .
  16. Дано целое положительное число . Вычислить произведение натуральных чисел кратных трём и не превышающих число .
  17. Задано целое положительное число . Определить значение выражения: .
  18. Вычислить сумму натуральных трёхзначных чисел кратных пяти и не делящихся на десять.
  19. Определить значение выражения: .
  20. Вычислить количество натуральных двузначных нечётных чисел не делящихся на 5.
  21. Задано целое положительное число . Определить значение выражения: .
  22. Задано целое положительное число . Определить значение выражения: .
  23. Найти произведение чётных степеней двойки. Значение степени изменяется от 0 до 8.
  24. Вычислить произведение натуральных чисел не превышающих 15.
  25. Вычислить произведение натуральных двузначных чисел кратных трём и не делящихся на 10.

3.6.6 Циклический процесс. Последовательности произвольных чисел

Разработать программу на языке С++ для следующих заданий:

  1. Вводится последовательность ненулевых чисел, 0 — конец последовательности. Определить сумму положительных элементов последовательности.
  2. Вычислить сумму отрицательных элементов последовательности из произвольных чисел.
  3. Вводится последовательность ненулевых чисел, 0 — конец последовательности. Определить, сколько раз последовательность поменяет знак.
  4. В последовательности из произвольных чисел подсчитать количество нулей.
  5. Вводится последовательность ненулевых чисел, 0 — конец последовательности. Определить наибольшее число в последовательности.
  6. Вводится последовательность из произвольных чисел найти наименьшее число в последовательности.
  7. Вводится последовательность ненулевых чисел, 0 — конец последовательности. Определить среднее значение элементов последовательности.
  8. Вводится последовательность из произвольных чисел, найти среднее значение положительных элементов последовательности.
  9. Вводится последовательность ненулевых чисел, 0 — конец последовательности. Подсчитать процент положительных и отрицательных чисел.
  10. Вводится последовательность из произвольных чисел. Определить процент положительных, отрицательных и нулевых элементов.
  11. Вводится последовательность из произвольных чисел. Вычислить разность между наименьшим и наибольшим значениями последовательности.
  12. Вводится последовательность из положительных целых чисел. Найти наименьшее число среди чётных элементов последовательности.
  13. Вводится последовательность из целых чисел. Определить, является ли эта последовательность знакочередующейся.
  14. Определить, является ли последовательность из произвольных чисел строго возрастающей (каждый следующий элемент больше предыдущего).
  15. Вводится последовательность произвольных чисел, 0 — конец последовательности. Определить, является ли эта последовательность строго убывающей (каждый следующий элемент меньше предыдущего).
  16. Вводится последовательность ненулевых целых чисел, 0 — конец последовательности. Определить среднее значение чётных элементов последовательности.
  17. Вводится последовательность из произвольных чисел, найти среднее значение отрицательных элементов последовательности.
  18. В последовательности из целых чисел подсчитать количество чётных и нечётных чисел.
  19. Вводится последовательность целых чисел, 0 — конец последовательности. Определить процент чётных и нечётных чисел в последовательности.
  20. Вводится последовательность из целых чисел. Определить, содержит ли последовательность хотя бы два соседних одинаковых числа.
  21. Вводится последовательность целых чисел, 0 — конец последовательности. Определить наибольшее число среди нечётных элементов последовательности.
  22. Вводится последовательность произвольных чисел, 0 — конец последовательности. Определить сумму и количество чисел в последовательности.
  23. Вводится последовательность из произвольных чисел. Найти сумму положительных и сумму отрицательных элементов последовательности.
  24. Вводится последовательность произвольных чисел, 0 — конец последовательности. Определить отношение минимального и максимального элементов друг к другу.
  25. Вводится последовательность из целых чисел. Определить количество одинаковых рядом стоящих чисел.

3.6.7 Циклический процесс. Работа с цифрами в числе

Разработать программу на языке С++ для следующих заданий:

  1. Определить, является ли целое положительное число совершённым. Совершённое число равно сумме всех своих делителей, не превосходящих это число. Например, 6=1+2+3 или 28=1+2+4+7+14.
  2. Проверить, является ли пара целых положительных чисел дружественными. Два различных натуральных числа являются дружественными, если сумма всех делителей первого числа (кроме самого числа) равна второму числу. Например, 220 и 284, 1184 и 1210, 2620 и 2924, 5020 и 5564.
  3. Определить, является ли целое положительное число недостаточным. Недостаточное число всегда больше суммы всех своих делителей за исключением самого числа.
  4. Вводится целое положительное число. Определить количество чётных и нечётных цифр в числе.
  5. Вводится целое положительное число. Найти число, которое равно сумме кубов цифр исходного числа.
  6. Вводится целое положительное число. Определить, совпадает ли сумма цифр, расположенных до середины числа, с суммой цифр расположенных после. Например, задано число из восьми цифр 12112021. Здесь, сумма первых четырёх цифр, равна сумме следующих четырёх цифр 1 + 2 + 1 + 1 = 2+0+2+1 = 5. Или, задано число из семи цифр 3456444, тогда 3+4+5 = 4 + 4 + 4 = 12. Здесь цифра 6 не учитывается.
  7. Вводится целое положительное число. Найти суммы чётных и нечётных цифр заданного числа.
  8. Задано целое положительное число. Определить количество его чётных и нечётных делителей.
  9. Проверить, являются ли два целых положительных числа взаимно простыми. Два различных натуральных числа являются взаимно простыми, если их наибольший общий делитель равен единице.
  10. Определить, является ли целое положительное число составным. Составное число имеет более двух делителей, то есть не является простым.
  11. Вводится целое положительное число. Найти наименьшую цифру числа.
  12. Задано целое положительное число. Определить, является ли оно числом Армстронга. Число Армстронга — натуральное число, которое равно сумме своих цифр, возведённых в степень, равную количеству его цифр. Например, десятичное число 153 — число Армстронга, потому что: 13 + 33 + 53 = 1 + 27 + 125 = 153.
  13. Вводится целое положительное число. Найти произведение всех ненулевых цифр числа.
  14. Вводится целое положительное число. Найти наибольшую цифру числа.
  15. Вводится целое положительное число. Определить позицию наибольшей цифры в числе.
  16. Вводится целое положительное число. Найти число, которое равно сумме удвоенных цифр исходного числа.
  17. Вводится целое положительное число. Найти число, которое равно сумме квадратов цифр исходного числа.
  18. Задано целое положительное число. Определить сумму его делителей.
  19. Вводится целое положительное число. Определить позицию наименьшей цифры в числе.
  20. Проверить, что два целых положительных числа не являются взаимно простыми. Различные натуральные числа не являются взаимно простыми, если их наибольший общий делитель отличен от единицы.
  21. Убедиться, что заданное целое положительное число не является палиндромом. Числа-палиндромы симметричны относительно своей середины, например, 12021 или 454.
  22. Убедиться, что заданное целое положительное число не является совершённым. Совершённое число равно сумме всех своих делителей, не превосходящих это число. Например, 6=1+2+3 или 28=1+2+4+7+14.
  23. Проверить, что два целых положительных числа не являются дружественными. Два различных натуральных числа являются дружественными, если сумма всех делителей первого числа (кроме самого числа) равна второму числу. Например, 220 и 284, 1184 и 1210, 2620 и 2924, 5020 и 5564.
  24. Вводится целое положительное число. Найти число, которое равно сумме утроенных цифр исходного числа.
  25. Вводятся два целых положительных числа. Найти сумму их цифр.

3.6.8 Вложенные циклы

Разработать программу на языке С++ для следующих заданий:

  1. Дано натуральное число . Вывести все простые числа не превосходящие .
  2. Дано натуральное число . Вывести все совершённые числа не превосходящие .
  3. Вводится последовательность положительных целых чисел, 0 — конец последовательности. Определить количество совершённых чисел в последовательности.
  4. Вводится последовательность положительных целых чисел, 0 — конец последовательности. Определить количество простых чисел в последовательности.
  5. Вводится последовательность из положительных целых чисел. Для каждого элемента последовательности вычислить факториал.
  6. Вводится последовательность из положительных целых чисел. Вывести на экран все числа — палиндромы. Если таких чисел нет, выдать соответствующее сообщение.
  7. Вводится последовательность из положительных целых чисел. Определить разрядность каждого числа.
  8. Вводится последовательность из положительных целых чисел. Вывести на экран количество делителей каждого числа.
  9. Вводится последовательность положительных целых чисел, 0 — конец последовательности. Определить сумму цифр каждого элемента последовательности.
  10. Дано наборов ненулевых целых чисел. Признаком завершения каждого набора является число 0. Для каждого набора вывести количество его элементов. Вычислить общее количество элементов.
  11. Дано наборов ненулевых целых чисел. Признаком завершения каждого набора является число 0. Для каждого набора вычислить среднее арифметическое его элементов.
  12. Даны наборов целых чисел по элементов в каждом наборе. Для каждого набора найти наибольшее значение его элементов.
  13. Даны наборов целых чисел по элементов в каждом наборе. Определить, есть ли среди наборов данных знакочередующиеся последовательности.
  14. Даны наборов целых чисел по элементов в каждом наборе. Определить, есть ли среди наборов данных строго возрастающие последовательности.
  15. Дано наборов ненулевых целых чисел. Признаком завершения каждого набора является число 0. Для каждого набора найти наименьшее значение его элементов.
  16. Даны наборов целых чисел по элементов в каждом наборе. Для каждого набора вычислить произведение ненулевых элементов.
  17. Даны наборов целых чисел по элементов в каждом наборе. Найти наибольшее число для всех наборов.
  18. Дано наборов ненулевых целых чисел. Признаком завершения каждого набора является число 0. Вычислить среднее арифметическое всех элементов во всех наборах.
  19. Дано наборов ненулевых целых чисел. Признаком завершения каждого набора является число 0. Найти количество возрастающих наборов.
  20. Дано наборов ненулевых целых чисел. Признаком завершения каждого набора является число 0. Найти количество убывающих наборов.
  21. Дано наборов ненулевых целых чисел. Признаком завершения каждого набора является число 0. Найти количество наборов не являющихся знакочередующимися.
  22. Дано наборов ненулевых целых чисел. Признаком завершения каждого набора является число 0. Найти количество наборов элементы которых не возрастают и не убывают.
  23. Даны целые положительные числа и . Вывести все целые числа от до включительно; при этом каждое число должно выводиться столько раз, каково его значение (например, число 5 выводится 5 раз).
  24. Дано целое число . Найти сумму
  25. Даны целые числа и . Вывести все целые числа от до включительно; при этом число должно выводиться 1 раз, число должно выводиться 2 раза и т. д.

Лекция 4. Использование функций при программировании на C++

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

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

Подпрограмма — именованная, логически законченная группа операторов языка, которую можно вызвать для выполнения любое количество раз из различных мест программы. В языке C++ подпрограммы реализованы в виде функций [4 ]

4.1. Общие сведения о функциях. Локальные и глобальные переменные

Функция — это поименованный набор описаний и операторов, выполняющих определённую задачу. Функция может принимать параметры и возвращать значение. Информация, передаваемая в функцию для обработки, называется параметром, а результат вычисления функции её значением. Обращение к функции называют вызовом. Как известно (п. 2.8), любая программа на C++ состоит из одной или нескольких функций. При запуске программы первой выполняется функция main. Если среди операторов функции main встречается вызов функции, то управление передаётся операторам функции. Когда все операторы функции будут выполнены, управление возвращается оператору, следующему за вызовом функции.

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

тип имя_функции(список_переменных)
{
	тело_функции
}
		

Заголовок функции содержит:

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

В общем виде структура программы на C++ может иметь вид:

директивы компилятора
тип имя_1(список_переменных)
{
	тело_функции_1;
}
тип имя_2(список_переменных)
{
	тело_функции_2;
}
...
тип имя_n(список_переменных)
{
	тело_функции_n;
}
int main (список_переменных)
{
	//Тело функции может содержать операторы вызова функций имя_1, имя_2, ..., имя_n
	тело_основной_функции;
}
		

Однако допустима и другая форма записи программного кода :

директивы компилятора
тип имя_1(список_переменных);
тип имя_2(список_переменных);
...
тип имя_n(список_переменных);
int main (список_переменных)
{
	//Тело функции может содержать операторы вызова функций имя_1, имя_2, ..., имя_n
	тело_основной_функции;
}
тип имя_1(список_переменных)
{
	тело_функции_1;
}
тип имя_2(список_переменных)
{
	тело_функции_2;
}
...
тип имя_n(список_переменных)
{
	тело_функции_n;
}
		

Здесь функции описаны после функции main(), однако до неё перечислены заголовки всех функций. Такого рода опережающие заголовки называют прототипами функций. Прототип указывает компилятору тип данных, возвращаемых функцией, тип переменных, выступающих в роли аргументов, и порядок их следования. Прототипы используются для проверки правильности вызова функций в основной программе. Имена переменных, указанные в прототипе функции, компилятор игнорирует:

//Записи равносильны.
int func ( int a, int b );
int func ( int, int );
		

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

имя_функции(список_переменных);
		

Рассмотрим пример. Создадим функцию f(), которая не имеет входных значений и не формирует результат. При вызове этой функции на экран выводится строка символов "С Новым Годом, ".

#include <iostream>
using namespace std;
void f ( ) //Описание функции.
{
	cout << "С Новым Годом, ";
}
int main ( )
{
	f ( ); //Вызов функции.
	cout <<"Студент!" << endl;
	f ( ); //Вызов функции.
	cout <<"Преподаватель!" << endl;
	f ( ); //Вызов функции.
	cout <<"Народ!" << endl;
}
		

Результатом работы программы будут три строки:

С Новым Годом, Студент!
С Новым Годом, Преподаватель!
С Новым Годом, Народ!
		

Далее приведён пример программы, которая пять раз выводит на экран фразу "Здравствуй, мир!". Операция вывода строки символов оформлена в виде функции fun(). Эта функция также не имеет входных значений и не формирует результат. Вызов функции осуществляется в цикле:

#include <iostream>
using namespace std;
void fun ( )
{
	cout << "Здравствуй, мир!" << endl;
}
int main ( )
{
	for ( int i =1; i <=5; fun ( ), i ++);
}
		

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

return (выражение);
		

Далее приведён пример программы, которая вычисляет значение выражения при заданном значении α. Здесь функция radian выполняет перевод градусной меры угла в радианную1).

#include <iostream>
#include <math.h>
#define PI 3.14159
using namespace std;
double radian ( int deg, int min, int sec )
{
	return ( deg * PI/180+min* PI /180/60+ sec * PI /180/60/60);
}
int main ( )
{
	int DEG, MIN, SEC; double RAD;
	//Ввод данных.
	cout<<" Inpout :"<<endl; //Величина угла:
	cout<<" DEG ="; cin >>DEG; //градусы,
	cout<<" MIN ="; cin >>MIN; //минуты,
	cout<<" SEC ="; cin >>SEC; //секунды.
	//Величина угла в радианах.
	RAD=radian (DEG, MIN, SEC); //Вызов функции.
	cout << " Value in radian A="<<RAD << endl;
	//Вычисление значения выражения и его вывод.
	cout << " sin (A) ^2+ cos (A) ^2= ";
	cout << pow( sin (RAD), 2 )+pow( cos (RAD), 2 ) << endl;
	return 0;
}
			

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

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

4.2 Передача параметров в функцию

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

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

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

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

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

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

тип имя_функции ( const тип_переменной* имя_переменной, ...)
		

Например:

#include <iostream>
using namespace std;
int f1 ( int i ) //Данные передаются по значению
{
	return ( i ++);
}
int f2 ( int * j ) //Данные передаются по адресу. При подстановке фактического параметра,
//для получения его значения, применяется операция разадресации * .
{
	return ( ( * j )++);
}
int f3 ( const int * k ) //Изменение параметра не предусмотрено .
{
	return ( ( * k )++);
}
int main ( )
{
	int a;
	cout<<"a="; cin >>a;
	f  ( a );
	cout<<"a="<<a<<"\n";
	f2 (&a ); //Для передачи фактического параметра используется операция взятия адреса & .
	cout<<"a="<<a<<"\n";
	f3 (&a );
	cout<<"a="<<a<<"\n";
	return 0;
}
		

Результат работы программы:

Введено значение переменной .

a=5
		

Значение переменной a после вызова функции не изменилось.

a=5
		

Значение переменной a после вызова функции изменилось.

a=6
		

Значение переменной a после вызова функции не изменилось.

a=6
		

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

Далее приведён пример программы, в которой исходя из радианной меры некоторого угла вычисляется величина смежного с ним угла. На экран выводятся значения углов в градусной мере. Функция degree выполняет перевод из радианной меры в градусную1). Эта функция ничего не возвращает. Её аргументами являются значение переменной rad, определяющее величину угла в радианах, и адреса переменных deg, min, sec, в которых будут храниться вычисленные результаты — градусная мера угла.

#include <iostream>
#include <math.h>
#define PI 3.14159
using namespace std;
void degree ( double rad, int * deg, int * min, int * s e c )
{
	* deg= floor ( rad * 180/ PI );
	* min=floor ( ( rad * 180/ PI -(* deg ) ) * 60);
	* sec=floor ( ( ( rad * 180/ PI -(* deg ) ) *60 -(* min ) ) * 60);
}
int main ( )
{
	int DEG, MIN, SEC; double RAD;
	cout<<" Inpout :"<<endl;
	cout << " Value in radian A="; cin >>RAD;
	degree (RAD,&DEG,&MIN,&SEC);
	cout << DEG<<" "<<MIN<<" "<<SEC << endl;
	degree ( PI- RAD,&DEG,&MIN,&SEC);
	cout << DEG<<" "<<MIN<<" "<<SEC << endl;
	return 0;
}
		

4.3 Возврат результата с помощью оператора return

Возврат результата из функции в вызывающую её функцию осуществляется оператором return выражение;

Работает оператор следующим образом. Вычисляется значение выражения, указанного после return, и преобразуется к типу возвращаемого функцией значения. Выполнение функции завершается, а вычисленное значение передаётся в вызывающую функцию. Любые операторы, следующие в функции за оператором return, игнорируются. Программа продолжает свою работу с оператора, следующего за оператором вызова данной функции.

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

Также функция может содержать несколько операторов return, если это определено потребностями алгоритма. Например, в следующей программе функция equation вычисляет корни квадратного уравнения. Если (уравнение не является квадратным), то в программу передаётся значение равное -1, если дискриминант отрицательный (уравнение не имеет действительных корней), то 1, а если положительный, то вычисляются корни уравнения и в программу передаётся 0.

#include <iostream>
#include <math.h>
using namespace std;
int equation ( float a, float b, float c, float * x1, float * x2 )
{ float D=b*-4*a * c;
	if ( a==0) return -1;
	else if (D<0) return 1;
		else
		{
		*x1=(-b+sqrt (D) ) /2/ a;
		*x2=(-b-sqrt (D) ) /2/ a;
		return 0;
		}
}
int main ( )
{
	float A, B, C, X1, X2; int P;
	cout<<" Enter the coefficients of the equation :"<<endl;
	cout<<"A="; cin >>A;
	cout<<"B="; cin >>B;
	cout<<"C="; cin >>C;
	P=equation ( A, B, C, &X1, &X2);
	if (P==-1) cout<<" input Error "<<endl;
	else if (P==1) cout<<" No real roots "<<endl;
		else cout<<" X1="<<X1<<" X2="<<X2<<endl;
	return 0;
}
		

4.4 Решение задач с использованием функций

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

Задача 4.1. Вводится последовательность из целых чисел, найти среднее арифметическое совершённых чисел и среднее геометрическое простых чисел последовательности.

Напомним, что целое число называется простым, если оно делится нацело только на само себя и единицу. Подробно алгоритм определения простого числа описан в задаче 3.15 (рис. 3.29). В этой задаче кроме простых чисел фигурируют совершённые числа. Число называется совершённым, если сумма всех делителей, меньших его самого, равна этому числу. Алгоритм, с помощью которого можно определить делители числа, подробно рассмотрен в задаче 3.14 (рис. 3.28).

Для решения поставленной задачи понадобятся две функции:

#include <iostream>
#include <math.h>
unsigned int prostoe ( unsigned int N) //Описание функции.
{
	//Функция определяет, является ли число простым.
	int i, pr;
	for ( pr =1, i =2; i<=N/ 2; i ++)
	if (N%i ==0) { pr =0; break; }
	return pr;
}
unsigned int soversh ( unsigned int N) //Описание функции.
{
	//Функция определяет, является ли число совершённым.
	unsigned int i, S;
	for ( S=0, i =1; i<=N/ 2; i ++)
		if (N%i ==0) S+=i; //Сумма делителей.
	if ( S==N) return 1;
	else return 0;
}
using namespace std;
int main ( )
{
	unsigned int i,N,X, S, kp, ks;
	long int P;
	cout <<"N="; cin >>N;
	for ( kp=ks=S=0,P=1, i =1; i<=N; i ++)
	{
		cout <<"X="; cin >> X; //Вводится элемент последовательности.
		if ( prostoe (X) ) // X — простое число.
		{
			kp++; //Счётчик простых чисел.
			P*=X; //Произведение простых чисел.
		}
		if ( soversh (X) ) //X — совершённое число.
		{
			ks++; //Счётчик совершённых чисел.
			S+=X; //Сумма совершённых чисел.
		}
	}
	if ( kp>0) //Если счётчик простых чисел больше нуля,
		//считаем среднее геометрическое и выводим его,
		cout<<"Среднее геометрическое="<<pow(P, ( float ) 1/kp )<<endl;
	else //в противном случае –– сообщение об отсутствии простых чисел.
		cout<<"Нет простых чисел\n";
	if ( ks>0) //Если счётчик совершённых чисел больше нуля,
		//считаем среднее арифметическое и выводим его,
		cout<<"Среднее арифметическое="<<(float ) S/ ks<<endl;
	else //в противном случае — сообщение об отсутствии совершённых чисел.
		cout<<"Нет совершённых чисел\n";
	return 0;
}
		

Задача 4.2. Вводится последовательность целых чисел, 0 — конец последовательности. Найти минимальное число среди простых чисел и максимальное — среди чисел, не являющихся простыми.

Для решения данной задачи создадим функцию prostoe, которая будет проверять, является ли число простым. Входным параметром функции будет целое положительное число . Функция будет возвращать значение 1, если число простое, и 0 — в противном случае. Алгоритм поиска максимума (минимума) подробно описан в задаче 3.20 (рис. 3.31).

Текст программы:

#include <iostream>
using namespace std;
unsigned int prostoe ( unsigned int N)
{
	int i, pr;
	for ( pr =1, i =2; i>=N/ 2; i ++)
	if (N%i ==0) { pr =0; break; }
	return pr;
}
int main ( )
{
	int kp=0,knp=0,min, max,N;
	for ( cout << "N=", cin >>N; N! =0; cout<<"N=", cin >>N)
		//В цикле вводится элемент последовательности N.
		if ( prostoe (N) ) //N — простое число,
		{
			kp++; //счётчик простых чисел.
			if ( kp==1) min=N; //Предполагаем, что первое простое число минимально,
			else if (N<min ) min=N; //если найдётся меньшее число, сохраняем его.
		}
		else //N — простым не является,
		{
			knp++; //счётчик чисел не являющихся простыми.
			if ( knp==1) max=N; //Предполагаем, что первое не простое число максимально,
			else if (N>max) max=N; //если найдётся большее число, сохраняем его.
		}
		if ( kp>0) //Если счётчик простых чисел больше нуля,
			cout <<" min = "<<min<<"\t"; //выводим значение минимального простого числа,
		else //в противном случае —
			cout <<"Нет простых чисел"; //сообщение об отсутствии простых чисел.
		if ( knp>0) //Если счётчик чисел не являющихся простыми больше нуля,
			cout <<" max ="<<max<<endl; //выводим значение максимального числа,
		else //в противном случае —
			cout <<"Нет составных чисел"; //сообщение об отсутствии чисел
			//не являющихся простыми.
	return 0;
}
		

Задача 4.3. Вводится последовательность из целых положительных чисел. В каждом числе найти наименьшую и наибольшую цифры1).

Программный код к задаче 4.3.

#include <iostream>
using namespace std;
unsigned int Cmax( unsigned long long int P)
{
	unsigned int max;
	if (P==0) max=0;
	for ( int i =1; P! =0;P/=10)
	{
		if ( i ==1) {max=P%10; i ++;}
		if (P%10>max) max=P%10;
	}
	return max;
}
unsigned int Cmin( unsigned long long int P)
{
	unsigned int min;
	if (P==0) min=0;
	for ( int i =1; P! =0;P/=10)
	{
		if ( i ==1) {min=P%10; i ++;}
		if (P%10<min ) min=P%10;
	}
	return min;
}
int main ( )
{
	unsigned int N, k;
	unsigned long long int X;
	for ( cout<<"N=", cin >>N, k=1;k<=N; k++)
	{
		cout<<"X="; cin >>X;
		cout<<"Максимальная цифра="<<Cmax(X);
		cout<<" Минимальная цифра="<<Cmin(X)<<endl;
	}
	return 0;
}
		

Задача 4.4. Вводится последовательность целых положительных чисел, 0 — конец последовательности. Определить, сколько в последовательности чисел-палиндромов2).

Алгоритм определения палиндрома подробно описан в задаче 3.19. Далее приведён программный код к задаче 4.4

#include <iostream>
using namespace std;
bool palindrom ( unsigned long long int N)
{ //Функция определяет, является ли число N палиндромом, возвращает true, если N —
	//палиндром, и false в противном случае
	unsigned long int M, R, S;
	int kol, i;
	for (R=1, kol =1,M=N;M/10>0; kol ++,R*=10,M/=10);
	for ( S=0,M=N, i =1; i<=kol; S+=M%10*R,M/=10,R/=10, i ++);
		if (N==S ) return true;
		else return false;
}
int main ( )
{ unsigned long long int X;
	int K;
	for (K=0, cout<<"X=", cin >>X;X! =0; cout<<"X=", cin >>X)
		if ( palindrom (X) ) K++;
			cout<<"Количество палиндромов равно K="<<K<<endl;
	return 0;
}
		

Задача 4.5. Заданы два числа — в двоичной системе счисления, в системе счисления с основанием пять. Найти сумму этих чисел. Результат вывести в десятичной системе счисления.

Любое целое число , заданное в -ичной системе счисления, можно представить в десятичной системе счисления:

где — основание системы счисления (целое положительное фиксированное число), — разряд числа: Например,

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

#include <iostream>
using namespace std;
unsigned long long int DecNC( unsigned long long int N, unsigned int b )
{
	//Функция выполняет перевод числа N, заданного в b-ичной системе счисления,
	//в десятичную систему счисления
	unsigned long long int S, P;
	for ( S=0,P=1;N! =0; S+=N%10*P, P*=b,N/=10);
	return S;
}
int main ( )
{
	unsigned long long int X,Y; unsigned int bX, bY;
	cout<<"X="; cin >>X; //Ввод числа X.
	cout<<"b="; cin >>bX; //Ввод основания с/с.
	cout<<"Y="; cin >>Y; //Ввод числа X.
	cout<<"b="; cin >>bY; //Ввод основания с/с.
	//Вывод заданных чисел в десятичной с/с.
	cout<<X<<"("<<bX<<")="<<DecNC(X, bX)<<" (10) "<<endl;
	cout<<Y<<"("<<bY<<")="<<DecNC(Y, bY)<<" (10) "<<endl;
	//Вычисление суммы и вывод результата.
	cout<<X<<"("<<bX<<")+"<<Y<<"("<<bY<<")=";
	cout<<DecNC(X, bX)+DecNC(Y, bY)<<" (10) "<<endl;
	return 0;
}
		

Задача 4.6. Задано число в десятичной системе счисления. Выполнить перевод числа в системы счисления с основанием 2, 5 и 7.

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

  1. Разделить данное число на основание новой системы счисления: остаток от деления — младший разряд нового числа;
  2. Если частное от деления не равно нулю, продолжать деление, как указано в п.1.

На рис. 4.1 приведён пример "ручного" перевода числа 256, заданного в десятичной системе счисления, в восьмеричную. В результате получим

Пример перевода числа в новую систему счисления


Рис. 4.1.  Пример перевода числа в новую систему счисления

Далее приведён текст программы, реализующей решение задачи 4.6.

#include <iostream>
using namespace std;
unsigned long long int NC( unsigned long long int N, unsigned int b )
{
	unsigned long long int S, P;
	for ( S=0,P=1;N! =0; S+=N%b *P, P*=10,N/=b );
	return S;
}
int main ( )
{
	unsigned long long int X;
	cout<<"X="; cin >>X; //Ввод числа X.
	//Перевод числа X в заданные системы счисления.
	cout<<X<<" (10) ="<<NC(X, 2 )<<" (2) "<<endl;
	cout<<X<<" (10) ="<<NC(X, 5 )<<" (5) "<<endl;
	cout<<X<<" (10) ="<<NC(X, 7 )<<" (7) "<<endl;
	return 0;
}
		

Задача 4.7. Найти корни уравнения .

Для решения задачи использовать:

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

Вообще говоря, аналитическое решение уравнения

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

Интервал можно выделить, изобразив график функции, или каким-либо другим способом. Но все способы основаны на следующем свойстве непрерывной функции: если функция непрерывна на интервале [] и на его концах имеет различные знаки, , то между точками имеется хотя бы один корень. Будем считать интервал настолько малым, что в нём находится только один корень. Рассмотрим самый простой способ уточнения корней.

Графическое решение задачи 4.7 показано на рис. 4.2. Так как функция дважды пересекает ось абсцисс, можно сделать вывод о наличии в уравнении двух корней. Первый находится на интервале [-0.4; -0.2], второй принадлежит отрезку [0.2; 0.4] .

Геометрическое решение задачи 4.7


Рис. 4.2.  Геометрическое решение задачи 4.7

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

Метод половинного деления (дихотомии). Пусть был выбран интервал изоляции [] (рис. 4.3). Примем за первое приближение корня точку c, которая является серединой отрезка [].Далее будем действовать по следующему алгоритму:

  1. Находим точку ;
  2. Находим значение ;
  3. Если , то корень лежит на интервале [], иначе корень лежит на интервале [];
  4. Если величина интервала меньше либо равна , то найден корень с точностью , иначе возвращаемся к п.1.

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

Блок-схема алгоритма решения уравнения методом дихотомии приведена на рис. 4.4. Понятно, что здесь — корень заданного уравнения.

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

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


Рис. 4.3.  Графическая интерпретация метода половинного деления

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

  1. функция непрерывна вместе со своими производными первого и второго порядка. Функция на концах интервала [] имеет разные знаки ;
  2. первая и вторая производные и сохраняют определённый знак на всём интервале [] .

Метод хорд. Этот метод отличается от метода дихотомии тем, что очередное приближение берём не в середине отрезка, а в точке пересечения с осью (рис. 4.5) прямой, соединяющей точки () и ().

Запишем уравнение прямой, проходящей через точки с координатами () и () :

(4.2)

Прямая, заданная уравнением (4.2), пересекает ось при условии .

Найдём точку пересечения хорды с осью : итак, .

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

Алгоритм решения уравнения методом дихотомии


Рис. 4.4.  Алгоритм решения уравнения методом дихотомии

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

Метод касательных (метод Ньютона). В одной из точек интервала [], пусть это будет точка , проведём касательную (рис. 4.7). Запишем уравнение этой прямой:

(4.3)

Так как эта прямая является касательной, и она проходит через точку , то .

Следовательно, .

Найдём точку пересечения касательной с осью :

Графическая интерпретация метода хорд


Рис. 4.5.  Графическая интерпретация метода хорд

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

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

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

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

Вычисление прекратить, если ( — точность).

Если неравенство выполняется на всём интервале [], то последовательность сходится к решению (т.е. l).

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

Алгоритм метода хорд


Рис. 4.6.  Алгоритм метода хорд

Уравнение (4.1) можно привести к виду следующим образом. Умножить обе части уравнения на число . К обеим частям уравнения добавить число . Получим . Это и есть уравнение вида , где

(4.4)

Необходимо чтобы неравенство выполнялось на интервале [], следовательно, и , а значит, с помощью подбора параметра можно добиться выполнения условия сходимости.

Для вычисления корней уравнения воспользуемся графическим решением (рис. 4.2) и определим интервал изоляции одного из корней, например, . Подберём значение , решив неравенство и и .

и, следовательно, .

Графическая интерпретация метода касательных


Рис. 4.7.  Графическая интерпретация метода касательных

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

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

Блок-схема метода простой итерации приведена на рис. 4.9.

Далее представлен текст программы, реализующий решение задачи 4.7.

#include <iostream>
#include <math.h>
using namespace std;
//Функция, определяющая левую часть уравнения f (x) = 0.
double f ( double x )
{
	return ( x *x-cos (5 * x ) );
}
//Функция, реализующая метод половинного деления.
int Dichotomy ( double a, double b, double * c, double eps )
{ int k=0;
	do
	{
		* c=(a+b ) / 2;
		if ( f ( * c ) * f ( a ) <0) b=*c;
		else a=*c;
		k++;
	}
	while ( fabs ( a-b )>=eps );
	return k;
}
//Функция, реализующая метод хорд.
int Chord ( double a, double b, double * c, double eps )
{ int k=0;
	do
	{
		* c=a-f ( a ) /( f ( b )-f ( a ) ) * ( b -a );
		if ( f ( * c ) * f ( a ) >0) a=*c;
		else b=*c;
		k++;
	}
	while ( fabs ( f ( * c ) )>=eps );
	return k;
}
double f1 ( double x ) //Первая производная функции f (x).
{
	return (2 * x+5* sin (5 * x ) );
}
double f2 ( double x ) //Вторая производная функции f (x).
{
	return (2+25* cos (5 * x ) );
}
//Функция, реализующая метод касательных.
int Tangent ( double a, double b, double * c, double eps )
{ int k=0;
	if ( f ( a ) * f2 ( a ) >0) * c=a;
	else * c=b;
	do
	{
		* c=*c-f (* c ) / f1 ( * c );
		k++;
	}
	while ( fabs ( f ( * c ) )>=eps );
	return k;
}
double fi ( double x, double L) //Функция, заданная выражением 4.4 .
{
	return ( x+L* f ( x ) );
}
//Функция, реализующая метод простой итерации.
int Iteration ( double *x, double L, double eps )
{ int k=0; double x0;
	do
	{
		x0=*x;
		* x= f i ( x0, L);
		k++;
	}
	while ( fabs ( x0-*x )>=eps );
	return k;
}
int main ( )
{
	double A, B, X, P;
	double ep =0.001; //Точность вычислений.
	int K;
	cout<<"a="; cin >>A; //Интервал изоляции корня.
	cout<<"b="; cin >>B;
	cout<<"Решение уравнения x^2 - cos (5* x) =0. "<<endl;
	cout<<"Метод дихотомии:"<<endl;
	K=Dichotomy (A, B,&X, ep );
	cout<<"Найденное решение x="<<X;
	cout<<", количество итераций k="<<K<<endl;
	cout<<"Метод хорд:"<<endl;
	K=Chord (A, B,&X, ep );
	cout<<" Найденное решение x="<<X;
	cout<<", количество итераций k="<<K<<endl;
	cout<<"Метод касательных:"<<endl;
	K=Tangent (A, B,&X, ep );
	cout<<" Найденное решение x="<<X;
	cout<<", количество итераций k="<<K<<endl;
	cout<<"Метод простой итерации:"<<endl;
	X=A;
	cout<<"L="; cin >>P;
	K=I t e r a t i o n (&X, P, ep );
	cout<<" Найденное решение x="<<X;
	cout<<", количество итераций k="<<K<<endl;
	return 0;
}
				

Алгоритм метода Ньютона


Рис. 4.8.  Алгоритм метода Ньютона

Алгоритм метода простой итерации


Рис. 4.9.  Алгоритм метода простой итерации

Результаты работы программы:

a=0.2
b=0.4
Решение уравнения x^2-cos(5*x)=0.
Метод дихотомии:
Найденное решение x=0.296094, количество итераций k=8
Метод хорд:
Найденное решение x=0.296546, количество итераций k=2
Метод касательных:
Найденное решение x=0.296556, количество итераций k=2
Метод простой итерации:
L=-0.2
Найденное решение x=0.296595, количество итераций k=3
				

4.5 Рекурсивные функции

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

Рассмотрим применение рекурсии на примерах [7, 8].

Задача 4.8. Вычислить факториал числа .

Вычисление факториала подробно рассмотрено в задаче 3.12 (рис. 3.25). Для решения этой задачи с применением рекурсии создадим функцию factoial, алгоритм которой представлен на рис. 4.10.

Рекурсивный алгоритм вычисления факториала


Рис. 4.10.  Рекурсивный алгоритм вычисления факториала

Текст программы с применением рекурсии:

#include <iostream>
using namespace std;
long int factorial ( int n )
{
	if ( n<=1)
	return n;
	else
	return n*factorial ( n-1);
}
int main ( )
{
	int i; long int f;
	cout<<"i="; cin >>i;
	f=factorial ( i );
	cout<<i <<" !="<<f<<"\n";
	return 0;
}
				

Задача 4.9. Вычислить -ю степень числа ( — целое число).

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

Для решения задачи создадим рекурсивную функцию stepen, алгоритм которой представлен на рис. 4.11.

Рекурсивный алгоритм вычисления степени числа


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

Текст программы с применением рекурсии:

#include <iostream>
using namespace std;
float stepen ( float a, int n )
{
	if ( n==0)
		return 1;
	else if ( n<0)
		return 1/ stepen ( a,-n );
	else
	return a * stepen ( a, n-1);
}
int main ( )
{
	int i; float s, b;
	cout<<"b="; cin >>b;
	cout<<"i="; cin >>i;
	s=stepen ( b, i );
	cout<<"s="<<s<<"\n";
	return 0;
}
				

Задача 4.10. Вычислить -е число Фибоначчи.

Если нулевой элемент последовательности равен нулю, первый — единице, а каждый последующий представляет собой сумму двух предыдущих, то это последовательность чисел Фибоначчи (0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ... ).

Алгоритм рекурсивной функции fibonachi изображён на рис. 4.12.

Рекурсивный алгоритм вычисления числа Фибоначчи


Рис. 4.12.  Рекурсивный алгоритм вычисления числа Фибоначчи

Текст программы:

#include <iostream>
using namespace std;
long int fibonachi ( unsigned int n )
{
	if ( ( n==0) | | ( n==1))
	return n;
	else
	return fibonachi ( n -1)+fibonachi ( n-2);
}
int main ( )
{
	int i; long int f;
	cout<<"i="; cin >>i;
	f=fibonachi ( i );
	cout<<"f="<<f<<"\n";
	return 0;
}
				

4.6 Перегрузка функций

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

В приведённом далее тексте программы три функции с именем Pow. Первая выполняет операцию возведения вещественного числа в дробную степень , где и — целые числа. Вторая возводит вещественное число в целую степень , а третья — целое число в целую степень1) . Какую именно функцию вызвать компилятор определяет по типу фактических параметров. Так, если — вещественное число, а — целое, то оператор Pow(a,k) вызовет вторую функцию, так как она имеет заголовок float Pow(float a, int n). Команда Pow((int)a,k) приведёт к вызову третьей функции float Pow(int a, int n), так как вещественная переменная преобразована к целому типу. Первая функция float Pow(float a, int k, int m) имеет три параметра, значит, обращение к ней осуществляется командой Pow(a,k,m).

#include <iostream>
using namespace std;
#include <math.h>
float Pow( float a, int k, int m) //Первая функция
{
	cout<<"Функция 1 \t";
	if ( a==0)
	return 0;
	else if ( k==0)
	return 1;
	else if ( a>0)
	return exp ( ( float ) k/m *log ( a ) );
	else if (m%2!=0)
	return -(exp ( ( float ) k/m *log (-a ) ) );
}
float Pow( float a, int n ) //Вторая функция
{
	float p; int i;
	cout<<"Функция 2 \t";
	if ( a==0)
		return 0;
	else if ( n==0)
		return 1;
	else if ( n<0)
	{
		n= -n;
		p=1;
		for ( i =1; i<=n; i ++)
		p*=a;
		return ( float ) 1/p;
	}
	else
	{
		p=1;
		for ( i =1; i<=n; i ++)
		p*=a;
		return p;
	}
}
float Pow( int a, int n ) //Третья функция
{
	int i, p;
	cout<<"Функция 3 \t";
	if ( a==0)
		return 0;
	else if ( n==0)
		return 1;
	else if ( n<0)
	{
		n= -n;
		p=1;
		for ( i =1; i<=n; i ++)
		p*=a;
		return ( float ) 1/p;
	}
	else
	{
		p=1;
		for ( i =1; i<=n; i ++)
		p*=a;
		return p;
	}
}
int main ( )
{
	float a; int k, n,m;
	cout<<"a="; cin >>a;
	cout<<"k="; cin >>k;
	cout<<"s="<<Pow( a, k )<<"\n"; //Вызов 2-й функции.
	cout<<"s="<<Pow ( ( int ) a, k )<<"\n"; //Вызов 3-й функции.
	cout<<"a="; cin >>a;
	cout<<"k="; cin >>k;
	cout<<"m="; cin >>m;
	cout<<"s="<<Pow( a, k,m)<<endl; //Вызов 1-й функции.
	return 0;
}
				

Результаты работы программы:

a=5.2
k=3
Функция 2 s=140.608
Функция 3 s=125
a=-8
k=1
m=1
Функция 1 s=-8
a=5.2
k=-3
Функция 2 s=0.00711197
Функция 3 s=0.008
a=-8
k=1
m=3
Функция 1 s=-2
				

4.7 Шаблоны функций

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

Простейшую функцию–шаблон в общем виде можно записать так [6]:

template <class Type> заголовок
{
	тело функции
}
				

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

Рассмотрим пример шаблона поиска наименьшего из четырёх чисел.

#include <iostream>
using namespace std;
//Определяем абстрактный тип данных с помощью служебного слова Type .
template <class Type>
Type minimum( Type a, Type b, Type c, Type d )
{ //Определяем функцию с использованием типа данных Type .
	Type min=a;
	if ( b<min ) min=b;
	if ( c<min ) min=c;
	if ( d<min ) min=d;
	return min;
}
int main ( )
{
	int ia, ib, i c, id, mini; float ra, rb, rc, rd, minr;
	cout<<" Vvod 4 thelih chisla \t";
	cin >>ia >>ib>>i c >>i d;
	mini=minimum( ia, ib, i c, i d ); //Вызов функции minimum, в которую передаём
	//4 целых значения.
	cout<<"\n"<<mini<<"\n";
	cout<<" Vvod 4 vecshestvenih chisla \t"; cin >>ra>>rb>>rc>>rd;
	minr=minimum( ra, rb, rc, rd ); //Вызов функции minimum, в которую передаём
	//4 вещественных значения.
	cout<<"\n"<<minr<<"\n";
	return 0;
}
				

4.8 Область видимости переменных в функциях

Как известно (п. 2.8), по месту объявления переменные в языке C++ делятся на три класса: локальные, глобальные и переменные, описанные в списке формальных параметров функций. Все эти переменные имеют разную область видимости.

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

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

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

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

::переменная;
				

Рассмотрим пример:

#include <iostream>
using namespace std;
float pr =100.678; //Переменная pr определена глобально.
int prostoe ( int n )
{
	int pr =1, i; //Переменная pr определена локально.
	if ( n<0) pr =0;
	else
	for ( i =2; i<=n / 2; i ++)
		if ( n%i ==0){ pr =0; break; }
			cout<<" local pr="<<pr<<"\n"; //Вывод локальной переменной.
	cout<<" global pr=" <<::pr<<"\n"; //Вывод глобальной переменной.
	return pr;
}
int main ( )
{
	int g;
	cout<<"g="; cin >>g;
	if ( prostoe ( g ) ) cout<<"g - prostoe \n";
	else cout<<"g - ne prostoe \n";
	return 0;
}
				

Результаты работы программы:

	g=7
	local pr=1 //Локальная переменная.
	global pr=100.678 //Глобальная переменная.
	g - prostoe
				

4.9 Функция main(). Параметры командной строки

Итак, любая программа на C++ состоит из одной или нескольких функций, причём одна из них должна обязательно носить имя main (основной, главный ). Именно этой функции передаётся управление после запуска программы. Как любая функция, main может принимать параметры и возвращать значения. У функции main две формы записи:

Первый параметр argc определяет количество параметров, передаваемых в функцию main из командной строки. Второй параметр argv — указатель на массив указателей типа char (массив строк). Каждый элемент массива ссылается на отдельный параметр командной строки. При стандартном запуске программы argc равно 1, argv — массив из одного элемента, этим элементом является имя запускаемого файла.

Рассмотрим следующую программу.

#include <iostream>
#include <stdlib .h>
using namespace std;
int main ( int argc, char * argv [ ] )
{
	int i;
	cout<<"В командной строке "<<argc<<" аргументов\n";
	for ( i =0; i <argc; i ++)
	cout<<"Аргумент № "<<i <<" "<<argv [ i ]<<endl;
	return 0;
}
				

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

В командной строке 1 аргументов
Аргумент № 0 ./1
				

Программа выводит количество параметров командной строки и последовательно все параметры. При стандартном запуске – количество аргументов командной строки — 1, этим параметром является имя запускаемого файла (в нашем случае, имя запускаемого файла — ./1).

Запустим программу следующим образом:

./1 abc 34 6 + 90 Вася Маша
				

Результаты работы программы представлены ниже.

В командной строке 8 аргументов
Аргумент № 0 ./1
Аргумент № 1 abc
Аргумент № 2 34
Аргумент № 3 6
Аргумент № 4 +
Аргумент № 5 90
Аргумент № 6 Вася
Аргумент № 7 Маша
				

Рассмотрим приложение, в которое в качестве параметров командной строки передаётся число1, операция, число2. Функция выводит

число1 операция число2.
				

Текст программы приведён на ниже1)

#include <iostream>
#include <stdlib .h>
#include <cstring>
using namespace std;
int main ( int argc, char **argv )
{
	//Если количество параметров больше или равно 4, то были введены два числа и знак операции.
	if ( argc >=4)
	//Если операция *, то выводим число1*число2.
	{
		if ( ! strcmp ( argv [ 2 ], "*" ) ) cout<<atof ( argv [ 1 ] ) * atof ( argv [ 3 ] )<<endl;
		else
		//Если операция +, то выводим число1+число2.
		if ( ! strcmp ( argv [ 2 ], "+" ) ) cout<<atof ( argv [ 1 ] )+atof ( argv [ 3 ] )<<endl;
		else
		//Если операция -, то выводим число1-число2.
		if ( ! strcmp ( argv [ 2 ], " -" ) ) cout<<atof ( argv [ 1 ] ) -atof ( argv [ 3 ] )<<endl;
		else
		//Если операция /, то выводим число1/число2.
		if ( ! strcmp ( argv [ 2 ], "/" ) ) cout<<atof ( argv [ 1 ] ) / atof ( argv [ 3 ] )<<endl;
		else cout<<"неправильный знак операции"<<endl;
	}
	else
	cout<<"недостаточное количество операндов"<<endl;
	return 0;
}
				

Ниже приведены варианты запуска программы и результаты её работы2). Предлагаем читателю самостоятельно разобраться с результатами всех тестовых запусков приложения.

./4 1.3 + 7.8
9.1
./4 1.3 - 7.8
-6.5
./4 1.3 / 7.8
0.166667
./4 1.3 \* 7.8
10.14
./4 1.3 % 7.8
неправильный знак операции
./4 1.3+ 7.8
недостаточное количество операндов
				

4.10 Задачи для самостоятельного решени

4.10.1 Применение функций при работе с последовательностями чисел

Разработать программу на языке C++ для следующих заданий:

  1. Вводится последовательность целых положительных чисел, 0 — конец последовательности. Для каждого элемента последовательности определить и вывести на экран число, которое получится после записи цифр исходного числа в обратном порядке.
  2. Вводится последовательность целых чисел, 0 — конец последовательности. Определить, содержит ли последовательность хотя бы одно совершённое число. Совершённое число равно сумме всех своих делителей, не превосходящих это число. Например, 6 = 1 + 2 + 3 или 28 = 1 + 2 + 4 + 7 + 14.
  3. Вводится последовательность из N целых положительных элементов. Определить, содержит ли последовательность хотя бы одно простое число. Простое число не имеет делителей, кроме единицы и самого себя.
  4. Вводится последовательность из целых положительных элементов. Посчитать количество чисел-палиндромов. Числа-палиндромы симметричны относительно своей середины, например, 12021 или 454.
  5. Вводится последовательность из целых положительных элементов. Подсчитать количество совершённых и простых чисел в последовательности.
  6. Поступает последовательность целых положительных чисел, 0 — конец последовательности. Определить, в каком из чисел больше всего делителей.
  7. Поступает последовательность целых положительных чисел, 0 — конец последовательности. Определить, в каком из чисел больше всего цифр.
  8. Вводится последовательность из целых положительных элементов. Проверить, содержит ли последовательность хотя бы одну пару соседних дружественных чисел. Два различных натуральных числа являются дружественными, если сумма всех делителей первого числа (кроме самого числа) равна второму числу. Например, 220 и 284, 1184 и 1210, 2620 и 2924, 5020 и 5564.
  9. Поступает последовательность целых положительных чисел, 0 — конец последовательности. Каждый элемент последовательности представляет собой номер m одного из чисел Фибоначчи. Вычислить и вывести на экран m-е число Фибоначчи. Напомним, что в последовательности Фибоначчи каждое последующее число представляет собой сумму двух предыдущих (0, 1, 1, 2, 3, 5, 8, 13, 21, 34,...). Например, четвёртое число Фибоначчи равно 3, седьмое — 13, а девятое — 34.
  10. Вводится последовательность из целых положительных элементов. Найти число с минимальным количеством цифр.
  11. Вводится последовательность из целых элементов. Для всех положительных элементов последовательности вычислить значение факториала. Вывести на экран число и его факториал.
  12. Поступает последовательность целых положительных чисел, 0 — конец последовательности. Вывести на экран все числа последовательности, являющиеся составными и их делители. Составное число имеет более двух делителей, то есть не является простым.
  13. Вводится последовательность из целых положительных элементов. Определить, содержит ли последовательность хотя бы одно число Армстронга. Число Армстронга — натуральное число, которое равно сумме своих цифр, возведённых в степень, равную количеству его цифр. Например, десятичное число 153 — число Армстронга, потому что: 13+ 33+ 53=1 + 27 + 125 = 153.
  14. Поступает последовательность целых положительных чисел, 0 — конец последовательности. Найти среднее арифметическое простых чисел в этой последовательности. Простое число не имеет делителей, кроме единицы и самого себя.
  15. Вводится последовательность из целых положительных элементов. Определить, сколько в последовательности пар соседних взаимно простых чисел. Различные натуральные числа являются взаимно простыми, если их наибольший общий делитель равен единице.
  16. В последовательности из целых положительных элементов найти сумму всех недостаточных чисел. Недостаточное число всегда больше суммы всех своих делителей за исключением самого числа.
  17. Вводится последовательность из целых положительных элементов. Посчитать количество элементов последовательности, имеющих в своём представлении цифру 0.
  18. Вводится пар целых положительных чисел и . В случае, если вычислить: .
  19. Вводится последовательность из целых элементов. Для каждого элемента последовательности найти среднее значение его цифр.
  20. Вводится последовательность целых положительных чисел, 0 — конец последовательности. Для каждого элемента последовательности определить и вывести на экран число, которое получится, если поменять местами первую и последнюю цифры исходного числа.
  21. Вводится последовательность из целых элементов. Для каждого элемента последовательности вывести на экран количество цифр и количество делителей.
  22. Вводится последовательность из целых положительных элементов. Среди элементов последовательности найти наибольшее число-палиндром. Числа-палиндромы симметричны относительно своей середины, например, 12021 или 454.
  23. Поступает последовательность целых положительных чисел, 0 — конец последовательности. Для каждого элемента последовательности вывести на экран сумму квадратов его цифр.
  24. Вводится последовательность из целых положительных элементов. Для простых элементов последовательности определить сумму цифр. Простое число не имеет делителей, кроме единицы и самого себя.
  25. Вводится последовательность целых положительных чисел, 0 — конец последовательности. Среди элементов последовательности найти наименьшее составное число. Составное число имеет более двух делителей, то есть не является простым.

4.10.2 Применение функций для вычислений в различных системах счисления

Разработать программу на языке C++ для решения следующей задачи. Заданы два числа — и , первое в системе счисления с основанием , второе в системе счисления с основанием . Вычислить значение по указанной формуле и вывести его на экран в десятичной системе счисления и системе счисления с основанием . Исходные данные для решения задачи представлены в табл. 4.1.

Таблица 4.1. Задания для решения задачи о различных системах счисления
ВариантpqCr
1283
2344
3465
4526
5647
6738
7825
8386
9472
10568
11653
12745
13837
14426
15583
16672
17768
18857
19243
20384
21475
22567
23658
24746
25832

4.10.3 Применение функций для решения нелинейных уравнений

Разработать программу на языке C++ для вычисления одного из корней уравнения методами, указанными в задании. Для решения задачи предварительно определить интервал изоляции корня графическим методом. Вычисления проводить с точностью . Оценить степень точности путём подсчёта количества итераций, выполненных для достижения заданной точности. Исходные данные для решения задачи представлены в табл. 4.2.

Таблица 4.2. Задания к задаче о решении нелинейных уравнений
Уравнение f (x) = 0Методы решения
1метод половинного деления, метод хорд
2метод касательных, метод простой итерации
3метод хорд, метод касательных
4метод дихотомии, метод простой итерации
5метод половинного деления, метод касательных
6метод хорд, метод простой итерации
7метод половинного деления, метод хорд
8метод касательных, метод простой итерации
9метод хорд, метод касательных
10метод дихотомии, метод простой итерации
11метод половинного деления, метод касательных
12метод хорд, метод простой итерации
13метод половинного деления, метод хорд
14метод касательных, метод простой итерации
15метод хорд, метод касательных
16метод дихотомии, метод простой итерации
17метод половинного деления, метод касательных
18метод хорд, метод простой итерации
19метод половинного деления, метод хорд
20метод касательных, метод простой итерации
21метод хорд, метод касательных
22метод дихотомии, метод простой итерации
23метод половинного деления, метод касательных
24метод хорд, метод простой итерации
25метод половинного деления, метод хорд

Лекция 5. Массивы

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

5.1 Статические массивы в С(С++)

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

Массив в табл. 5.1 имеет 8 элементов, каждый элемент сохраняет число вещественного типа. Элементы в массиве пронумерованы (нумерация массивов начинается с нуля). Такого рода массив, представляющий собой просто набор данных одного и того же типа, называют простым или одномерным массивом. Для доступа к данным, хранящимся в определённом элементе массива, необходимо указать имя массива и порядковый номер этого элемента, называемый индексом.

Таблица 5.1. Одномерный числовой массив
№ элемента массива01234567
Значение13.65-0.9516.788.09-11.769.075.13-25.64

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

Таблица 5.2. Двумерный числовой массив
1.5-0.91.87.09-1.76
3.60.56.70.09-1.33
13.650.9516.788.09-11.76
7.50.957.38.90.11

Если при описании массива определён его размер, то массив называют статическим. Рассмотрим работу с одномерными статическими массивами в языке С(С++). Двумерные массивы подробно описаны в следующей главе.

5.1.1 Описание статических массивов

Описать статический массив в С(С++) можно так:

тип имя_переменной [размерность];

размерность — количество элементов в массиве. Например:

int x[10]; //Описание массива из 10 целых чисел. Первый
//элемент массива имеет индекс 0, последний 9.
float a[20]; //Описание массива из 20 вещественных чисел.
//Первый элемент массива имеет индекс 0, последний 19.

Размерность массива и тип его элементов определяют объём памяти, который необходим для хранения массива. Рассмотрим ещё один пример описания массива:

const int n=15; //Определена целая положительная константа.
double B[n]; //Описан массив из 15 вещественных чисел.

При описании статического массива в качестве размерности можно использовать целое положительное число или предопределённую константу.

Элементы массива в С(С++) нумеруются с нуля. Первый элемент всегда имеет номер ноль, а номер последнего элемента на единицу меньше заданной при его описании размерности:

char C[5]; //Описан массив из 5 символов, нумерация от 0 до 4.

5.1.2 Основные операции над массивами

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

имя_массива [индекс]

Например:

const int n=15;
double C[n], S;
S=C[0]+C[n-1]; //Сумма первого и последнего элементов массива С.

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

тип имя_переменной[размерность]={элемент_0, элемент_1, ...};

Например:

float a[6] = { 1.2, ( float ) 3/4, 5./6, 6.1 };
//Формируется массив из шести вещественных чисел, значения элементам присваиваются по
//порядку. Элементы, значения которых не указаны (в данном случае a[4], a[5]), обнуляются:
//a[0]=1.2, a[1]=(float)3/4, a[2]=5./6, a[3]=6.1, a[4]=0, a[5]=0,
//для элементов a[1] и a[2] выполняется преобразование типов.

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

double x[30];

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

  1. В соответствии с синтаксисом языка С(С++) записать x[0].
  2. Применить операцию *x, так как адрес начала массива хранится в переменной x (по существу x — указатель на double).

Если к значению x добавить единицу (число 1), то мы сместимся на один элемент типа double. Таким образом, x+1 — адрес элемента массива x с индексом 1. К первому элементу массива x также можно обратиться двумя способами: x[1] или *(x+1). Аналогично, к элементу с индексом 2 можно обращаться либо x[2], либо *(x+2). Таким образом, получается, что к элементу с индексом i можно обращаться x[i] или *(x+i).

При обработке массива (независимо от способа обращения x[i] или *(x+i)) программист сам должен контролировать, существует ли элемент массива x[i] (или *(x+i)) и не вышла ли программа за границы массива.

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

5.2 Динамические массивы в С(С++)

Для создания динамического массива необходимо [1, 8]:

Для выделения памяти в С++ можно воспользоваться оператором new или функциями языка С — calloc, malloc, realloc. Все функции находятся в библиотеке stdlib.h.

5.2.1 Функция malloc

Функция malloc выделяет непрерывный участок памяти размером size байт и возвращает указатель на первый байт этого участка. Обращение к функции имеет вид:

void* malloc ( size_t size );

где size — целое беззнаковое значение1), определяющее размер выделяемого участка памяти в байтах. Если резервирование памяти прошло успешно, то функция возвращает переменную типа void*, которую можно преобразовать к любому необходимому типу указателя. Если выделить память невозможно, то функция вернёт пустой указатель NULL.

Например,

double *h; //Описываем указатель на double.
int k;
cin>>k; //Ввод целого числа k.
//Выделение участка памяти для хранения k элементов типа double.
//Адрес этого участка хранится в переменной h.
h=(double *) malloc ( k* sizeof ( double ) ); //h — адрес начала участка памяти,
//h + 1, h + 2, h + 3 и т. д. — адреса последующих элементов типа double

5.2.2 Функция calloc

Функция calloc предназначена для выделения и обнуления памяти.

void * calloc ( size_t num, size_t size );

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

Например,

float *h; //Описываем указатель на float.
int k;
cin>>k; //Ввод целого числа k.
//Выделение участка памяти для хранения k элементов типа float.
//Адрес этого участка хранится в переменной h .
h=( float *) calloc ( k, sizeof ( float ) ); //h — адрес начала участка памяти,
//h + 1, h + 2, h + 3 и т. д. — адреса последующих элементов типа float.

5.2.3 Функция realloc

Функция realloc изменяет размер ранее выделенного участка памяти. Обращаются к функции так:

char *realloc ( void *p, size_t size );

где p — указатель на область памяти, размер которой нужно изменить на size. Если в результате работы функции меняется адрес области памяти, то новый адрес вернётся в качестве результата. Если фактическое значение первого параметра NULL, то функция realloc работает так же, как и функция malloc, то есть выделяет участок памяти размером size байт.

5.2.4 Функция free

Для освобождения выделенной памяти используется функция free. Обращаются к ней так:

void free ( void *p );

где p — указатель на участок памяти, ранее выделенный функциями malloc, calloc или realloc.

5.2.5 Операторы new и delete

В языке С++ есть операторы new для выделения и free для освобождения участка памяти.

Для выделения памяти для хранения n элементов одного типа оператор new имеет вид [5]:

x=new type [ n ];

type — тип элементов, для которых выделяется участок памяти;

n — количество элементов;

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

При выделении памяти для одного элемента оператор new имеет вид:

x=new type;

Например,

float *x; //Указатель на тип данных float .
int n;
cin>>n; //Ввод n
//Выделение участка памяти для хранения n элементов типа float. Адрес этого участка хранится
//в переменной x; x+1, x+2, x+3 и т. д. — адреса последующих элементов типа float.

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

delete [ ] p;

p — указатель (адрес участка памяти, ранее выделенного с помощью оператора new).

5.3 Отличие статического и динамического массива

В чём же отличие статического и динамического массива?

Предположим, описан статический массив: double x[75];

Это означает, что выделен участок памяти для хранения 75 элементов типа double (массив из 75 элементов типа double). Адрес начала массива хранится в переменной x. Для обращения к -му элементу можно использовать конструкции x[i] или *(x+i). Если понадобится обрабатывать массив более, чем из 75 элементов, то придётся изменить описание и перекомпилировать программу. При работе с массивами небольшой размерности, большая часть памяти, выделенной под статический массив, будет использоваться вхолостую.

Допустим, задан динамический массив, например

double *x; //Указатель на double
int k;
cin>>k; //Вводим размер массива k.
//Выделение памяти для хранения динамического массива из k чисел.
x=new double [ k ]; //Адрес начала массива хранится в переменной x.
x=(double *) calloc ( k, sizeof ( float ) ); //Память можно будет выделить так
x=(double *) malloc ( k* sizeof ( float ) ); //или так

В этом случае, мы имеем указатель на тип данных double, вводим — размер динамического массива, выделяем участок памяти для хранения элементов типа double (массив из элементов типа double). Адрес начала массива хранится в переменной x. Для обращения к -му элементу можно использовать конструкции x[i] или *(x+i). В случае динамического массива мы сначала определяем его размер (в простейшем случае просто вводим размер массива с клавиатуры), а потом выделяем память для хранения реального количества элементов. Основное отличие статического и динамического массивов состоит в том, что в динамическом массиве выделяется столько элементов, сколько необходимо.

Имя массива (статического или динамического) это адрес начала выделенного для него участка памяти, значит обращаться к элементам массива можно двумя способами — x[i] или *(x+i).

5.4 Основные алгоритмы обработки массивов

Все манипуляции с массивами в С++ осуществляются поэлементно. Организовывается цикл, в котором происходит последовательное обращение к нулевому, первому, второму и т.д. элементам массива. В общем виде алгоритм обработки массива выглядит так, как показано на рис. 5.1.

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

Алгоритм обработки элементов массива


Рис. 5.1.  Алгоритм обработки элементов массива

5.4.1 Ввод-вывод элементов массива

Ввод и вывод массивов также осуществляется поэлементно. Блок-схемы алгоритмов ввода и вывода элементов массива X[N] изображены на рис. 5.2,рис. 5.3.

Рассмотрим несколько вариантов ввода массива:

//Вариант 1. Ввод массива с помощью функции scanf.
//При организации ввода используются специальные символы: табуляция — \t
//и переход на новую строку — \n.
#include <stdio.h>
int main ( )
{
float x [ 10 ]; int i, n;
printf ( " \n N = " ); scanf ( " % d ",&n ); //Ввод размерности массива.
printf ( " \n Введите элементы массива X \n " );
for ( i =0; i<n; i++)
scanf ( " % f ", x+ i ); //Ввод элементов массива в цикле.
//Обратите внимание, x + i — адрес i-го элемента массива.
return 0;
}

Алгоритм ввода массива X[N]


Рис. 5.2.  Алгоритм ввода массива X[N]

Результат работы программы:

N=3
Введите элементы массива X
1.2
-3.8
0.49

Алгоритм вывода массива X[N]


Рис. 5.3.  Алгоритм вывода массива X[N]

//Вариант 2. Ввод массива с помощью функции scanf и вспомогательной переменной b.
#include <stdio.h>
int main ( )
{
float x [ 10 ], b; int i, n;
printf ( " \n N = " ); scanf ( " % d ",&n ); //Ввод размерности массива.
printf ( " \n Массив X \n " );
for ( i =0; i<n; i++)
{
	printf ( " \n Элемент % d \t ", i ); //Сообщение о вводе элемента.
	scanf ( " % f ",&b ); //Ввод переменной b.
	x [ i ]=b; //Присваивание элементу массива значения переменной b .
}
return 0;
}

Результат работы программы:

N=4
Массив X
Элемент 0 8.7
Элемент 1 0.74
Элемент 2 -9
Элемент 3 78
//Вариант 3. Ввод динамического массива с помощью cin.
#include <iostream>
using namespace std;
int main ( )
{
	int *X,N, i;
	cout<<" \n N = "; cin>>N; //Ввод размерности массива.
	X=new int [N ]; //Выделение памяти для динамического массива из N элементов.
	for ( i =0; i<N; i++)
	{
		cout<<" \n X [ "<<i<<" ]= "; //Сообщение о вводе элемента.
		cin>>X [ i ]; //Ввод элементов массива в цикле.
	}
	delete [ ] X;
	return 0;
}

Результат работы программы:

N=4
X[0]=1
X[1]=2
X[2]=4
X[3]=5

Вывод статического или динамического массива можно осуществить несколькими способами:

//Вариант 1. Вывод массива в виде строки.
for ( i =0; i<n; i++) printf ( " % f \t ",X [ i ] );
//Вариант 2. Вывод массива в виде столбца.
for ( i =0; i<n; i++) printf ( " \n % f ",X [ i ] );
//Вариант 3. Вывод массива в виде строки.
for ( i =0; i<N; i++) cout <<" \t X [ "<<i<<" ]= "<<X [ i ];
//Вариант 4. Вывод массива в виде столбца.
for ( i =0; i<N; i++) cout <<" \n X [ "<<i<<" ]= "<<X [ i ];

5.4.2 Вычисление суммы элементов массива

Дан массив , состоящий из элементов. Найти сумму элементов этого массива. Процесс накапливания суммы элементов массива достаточно прост и практически ничем не отличается от суммирования значений некоторой числовой последовательности. Переменной присваивается значение, равное нулю, затем к переменной последовательно добавляются элементы массива . Блок-схема алгоритма расчёта суммы приведена на рис. 5.4.

Алгоритм вычисления суммы элементов массива


Рис. 5.4.  Алгоритм вычисления суммы элементов массива

Соответствующий алгоритму фрагмент программы будет иметь вид:

for ( S= i =0; i<N; i++)
	S+=X [ i ];
cout<<" S = "<<S<<" \n ";

5.4.3 Вычисление произведения элементов массива

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

Соответствующий фрагмент программы будет иметь вид:

for (P=1, i =0; i<N; i++)
	P*=X [ i ];
cout<<" P = "<<P<<" \n ";

Задача 5.1. Задан массив целых чисел. Найти сумму простых чисел и произведение отрицательных элементов массива.

Алгоритм решения задачи состоит из следующих этапов.

  1. Вводим массив .
  2. Для вычисления суммы в переменную записываем значение 0, для вычисления произведения в переменную записываем 1.
  3. В цикле ( изменяется от 0 до с шагом 1) перебираем все элементы массива , если очередной элемент массива является простым числом, добавляем его к сумме, а если очередной элемент массива отрицателен, то умножаем его на .
  4. Выводим на экран значение суммы и произведения.

Вычисление произведения элементов массива


Рис. 5.5.  Вычисление произведения элементов массива

Блок-схема решения задачи представлена на рис. 5.6. Для решения задачи применим функцию (prostoe) проверки, является ли число простым. Текст программы с подробными комментариями приведён далее.

#include <iostream>
using namespace std;
//Текст функции prostoe.
bool prostoe ( int N)
{
	int i;
	bool pr;
	if (N<2) pr=false;
	else
	for ( pr=true, i =2; i<=N/ 2; i++)
	if (N%i ==0)
	{
		pr=false;
		break;
	}
	return pr;
}
int main ( )
{
	int *X, i,N, S, P;
	cout<<"Введите размер массива "; cin>>N; //Ввод размерности массива.
	X=new int [N ]; //Выделение памяти для хранения динамического массива X.
	cout<<"Ведите массив X \n "; //Ввод массива X.
	for ( i =0; i<N; i++)
	{ cout<<" X ( "<<i<<" ) = "; cin>>X [ i ]; }
		for (P=1,S= i =0; i<N; i++) //В цикле перебираем все элементы массива
		{
			//Если очередной элемент массива — простое число, добавляем его к сумме.
			if ( prostoe (X [ i ] ) ) S+=X [ i ];
			//Если очередной элемент массива отрицателен, умножаем его на P.
			if (X [ i ] <0) P*=X [ i ];
		}
	cout << " S = " <<S<<" \t P = "<<P<< endl; 	//Вывод S и P на экран.
	delete [ ] X; //Освобождение занимаемой массивом X памяти.
	return 0;
}

Блок-схема алгоритма решения задачи 5.1


Рис. 5.6.  Блок-схема алгоритма решения задачи 5.1

Результаты работы программы представлены ниже.

Введите размер массива 10
Ведите массив Х
X(0)=-7
X(1)=-9
X(2)=5
X(3)=7
X(4)=2
X(5)=4
X(6)=6
X(7)=8
X(8)=10
X(9)=12
S=14 P=63

Задача 5.2. Дан массив , состоящий из целых положительных чисел. Записать все чётные по значению элементы массива в массив .

На рис. 5.7 представлен фрагмент алгоритма решения данной задачи. Здесь индексы массива хранятся в переменной , а для номеров массива зарезервирована переменная . Операция, выполняемая в блоке 1, означает, что в массиве может не быть искомых элементов. Далее организован цикл (блок 2), с помощью которого можно обращаться к элементам массива . Если условие в блоке 3 выполняется, то переменная увеличивается на единицу, а значение соответствующего элемента массива записывается в массив под номером (блок 4). Условный блок 5 необходим для того, чтобы проверить, выполнилось ли хотя бы раз условие поиска (блок 2). Если массив сформирован, то он выводится на экран (блоки 6, 7), в противном случае выдаётся соответствующее сообщение (блок 8).

Алгоритм решения задачи 5.2


увеличить изображение

Рис. 5.7.  Алгоритм решения задачи 5.2

Приведённый ниже фрагмент программы реализует описанный алгоритм:

for (m=-1, i =0; i<k; i++)
{
	if (A [ i ]%2==0) //Если элемент чётный, то
	{
		m++; //увеличить значение индекса массива В
		B [m]=A [ i ]; //и записать элемент в массив В.
	}
}
if (m>-1) //Если чётные элементы найдены, то распечатать сформированный массив.
	for ( i =0; i<=m; cout<<B [ i ]<<" \t ", i++);
else //иначе, выдать сообщение,
	cout<<"Массив B не сформирован!"<<endl;

5.4.4 Поиск максимального элемента в массиве и его номера

Дан массив X, состоящий из n элементов. Найти максимальный элемент массива и номер, под которым он хранится в массиве.

Алгоритм решения задачи следующий. Пусть в переменной с именем Max хранится значение максимального элемента массива, а в переменной с именем Nmax – его номер. Предположим, что нулевой элемент массива является максимальным, и запишем его в переменную Max, а в Nmax — его номер (то есть ноль). Затем все элементы, начиная с первого, сравниваем в цикле с максимальным. Если текущий элемент массива оказывается больше максимального, то записываем его в переменную Max, а в переменную Nmax – текущее значение индекса i. Процесс определения максимального элемента в массиве приведён в табл. 5.3 и изображён при помощи блок-схемы на рис. 5.8. Соответствующий фрагмент программы имеет вид:

for (Max=X [ 0 ], Nmax= i =0; i<n; i++)
	if (Max<X [ i ] )
	{
		Max=X [ i ];
		Nmax= i;
	}
cout<<" Max = "<<Max<<" \n ";
cout<<" Nmax = "<<Nmax<<" \n ";
Таблица 5.3. Определение максимального элемента и его номера в массиве
Номера элементов012345
Исходный массив473892
Значение переменной 477899
Значение переменной 122455

Поиск максимального элемента и его номера в массиве


Рис. 5.8.  Поиск максимального элемента и его номера в массиве

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

Текст программы поиска номера максимального элемента:

#include <iostream>
using namespace std;
int main ( )
{
	float *X;
	int i,N, nom;
	cout<<"Введите размер массива "; cin>>N; //Ввод размерности динамического массива
	X=new float [N ]; //Выделение памяти для хранения динамического массива X.
	cout<<"Введите элементы массива X \n "; //Ввод динамического массива X.
	for ( i =0; i<N; i++)
		cin>>X [ i ];
	//В переменной nom будем хранить номер максимального элемента.
	nom=0; //Предположим, что максимальным элементом является элемент с номером 0.
	for ( i =1; i<N; i++)
	//Если очередной элемент больше X[nom], значит nom не является номером максимального
	//элемента, элемент с номером i больше элемента X[nom], поэтому переписываем
	//число i в переменную nom.
		if (X [ i ]>X [ nom ] ) nom= i;
	cout << "Максимальный элемент= "<<X [ nom]<<", его номер= " << nom << endl;
	return 0;
}

Совет. Алгоритм поиска минимального элемента в массиве будет отличаться от приведённого выше лишь тем, что в условном блоке и, соответственно, в конструкции if текста программы знак поменяется с "<" на ">".

Рассмотрим несколько задач.

Задача 5.3. Найти минимальное простое число в целочисленном массиве x[N].

Эта задача относится к классу задач поиска минимума (максимума) среди элементов, удовлетворяющих условию. Подобные задачи рассматривались в задачах на обработку последовательности чисел. Здесь поступим аналогично. Блок-схема приведена на рис. 5.9.

Необходимо первое простое число объявить минимумом, а все последующие простые элементы массива сравнивать с минимумом. Будем в цикле последовательно проверять, является ли элемент массива простым числом (функция prostoe). Если X[i] является простым числом, то количество простых чисел (k) увеличиваем на 1 (k++), далее, проверяем, если k равен 1 (if (k==1)), то этот элемент объявляем минимальным (min=x[i]; nom=i;), иначе сравниваем его с минимальным (if (x[i]<min) {min=x[i];nom=i;}).

Текст программы:

#include <iostream>
using namespace std;
bool prostoe ( int N)
{ int i; bool pr;
	if (N<2) pr=false;
	else
	for ( pr=true, i =2; i<=N/ 2; i++)
		if (N%i ==0)
		{
			pr=false;
			break;
		}
	return pr;
}
int main ( int arg c, char **argv )
{
	int i, k, n, nom, min, *x;
	cout<<" n = "; cin>>n; //Ввод количества элементов в массиве.
	x=new int [ n ]; //Выделяем память для динамического массива x.
	cout<<"Введите элементы массива X "; //Ввод элементов массива.
	for ( i =0; i<n; i++)
		cin>>x [ i ];
	//С помощью цикла по переменной i, перебираем все элементы в массиве x,
	//k — количество простых чисел в массиве.
	for ( i=k=0; i<n; i++)
	//Проверяем, является ли очередной элемент массива простым числом.
		if ( prostoe ( x [ i ] ) ) //Если x[i] — простое число.
		{
				k++; //Увеличиваем счётчик количества простых чисел в массиве.
				//Если текущий элемент является первым простым числом в массиве,
				// объявляем его минимумом, а его номер сохраняем в перемнной nom.
				if ( k==1) {min=x [ i ]; nom= i; }
		else
			//Все последующие простые числа в массиве сравниваем с минимальным простым числом.
			//Если текущее число меньше min, перезаписываем его в переменную min,
			//а его номер — в переменную nom.
			if ( x [ i ]<min ) {min=x [ i ]; nom= i; }
		}
	//Если в массиве были простые числа, выводим значение и номер минимального простого числа.
	if ( k>0)
		cout<<" min = "<<min<<" \ tnom = "<<nom<<endl;
	//Иначе выводим сообщение о том, что в массиве нет простых чисел.
	else cout<<"Нет простых чисел в массиве"<<endl;
	return 0;
}

Блок-схема решения задачи 5.3


Рис. 5.9.  Блок-схема решения задачи 5.3

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

Задача 5.4. Найти минимальных чисел в вещественном массиве.

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

Найти два наименьших элемента в массиве. Фактически надо найти номера (nmin1, nmin2) двух наименьших элементов массива. На первом этапе надо найти номер минимального (nmin1) элемента массива. На втором этапе надо искать номер минимального элемента, при условии, что он не равен nmin1. Вторая часть очень похожа на предыдущую задачу (минимум среди элементов, удовлетворяющих условию, в этом случае условие имеет вид i!=nmin1).

Решение задачи с комментариями:

#include <iostream>
using namespace std;
int main ( int arg c, char **argv )
{
	int kvo, i, n, nmin1, nmin2;
	double *X;
	cout<<" n = "; cin>>n;
	X=new double [ n ];
	cout<<"Введите элементы массива X \n ";
	for ( i =0; i<n; i++)
		cin>>X [ i ];
	//Стандартный алгоритм поиска номера первого минимального элемента (nmin1).
	for ( nmin1=0, i =1; i<n; i++)
		if (X [ i ]<X [ nmin1 ] ) nmin1= i;
	//Второй этап — поиск номера минимального элемента среди элементов, номер
	//которых не совпадает nmin1. kvo — количество таких элементов.
	for ( kvo= i =0; i<n; i++)
		if ( i !=nmin1 ) //Если номер текущего элемента не совпадает с nmin1,
		{
			kvo++; //увеличиваем количество таких элементов на 1.
			//Номер первого элемента, индекс которого не равен nmin1,
			//объявляем номером второго минимального элемента.
			if ( kvo==1) nmin2= i;
			else
			//очередной элемент, индекс которого не равен nmin1, сравниваем с минимальным,
			//если он меньше, номер перезаписываем в переменную nmin2.
			if (X [ i ]<X [ nmin2 ] ) nmin2= i;
		}
	//Вывод двух минимальных элементов и их индексов.
	cout<<" nmin1 = "<<nmin1<<" \ tmin1 = "<<X [ nmin1]<< endl;
	cout<<" nmin2 = "<<nmin2<<" \ tmin2 = "<<X [ nmin2]<< endl;
	return 0;
}

По образу и подобию этой задачи можно написать задачу поиска трёх минимальных элементов в массиве. Первые два этапа (поиск номеров двух минимальных элементов в массиве) будут полным повторением кода, приведённого выше. На третьем этапе нужен цикл, в котором будем искать номер минимального элемента, при условии, что его номер не равен nmin1 и nmin2. Авторы настоятельно рекомендуют читателям самостоятельно написать подобную программу. Аналогично можно написать программу поиска четырёх минимальных элементов. Однако при этом усложняется и увеличивается код программы. К тому же, рассмотренный приём не позволит решить задачу в общем случае (найти k минимумов).

Для поиска минимумов в массиве можно поступить следующим образом. Будем формировать массив nmin, в котором будут храниться номера минимальных элементов массива x. Для его формирования организуем цикл по переменной от 0 до k-1. При каждом вхождении в цикл в массиве nmin элементов будет j-1 элементов,i и мы будем искать -й минимум (формировать -й элемент массива). Алгоритм формирования -го элемента состоит в следующем: необходимо найти номер минимального элемента в массиве x, исключая номера, которые уже хранятся в массиве nmin. Внутри цикла по необходимо выполнить такие действия. Для каждого элемента массива x (цикл по переменной ) проверить содержится ли номер в массиве nmin, если не содержится, то количество (переменная kvo) таких элементов увеличить на 1. Далее, если kvo равно 1, то это первый элемент, который не содержится в массиве nmin, его номер объявляем номером минимального элемента массива (nmin_temp=i;). Если kvo>1, сравниваем текущий элемент x[i] с минимальным (if (x[i]<X[nmin_temp]) nmin_temp=i;). Блок-схема алгоритма поиска минимальных элементов массива представлена на рис. 5.101).Далее приведён текст программы с комментариями.

#include <iostream>
using namespace std;
int main ( int arg c, char **argv )
{
	int p, j, i, n, *nmin, k, kvo, nmin_temp;
	bool pr;
	double *x;
	cout<<" n = "; cin>>n;
	x=new double [ n ];
	cout<<"Введите элементы массива Х\n ";
	for ( i =0; i<n; i++)
		cin>>x [ i ];
	cout<<"Введите количество минимумов\n "; cin>>k;
	nmin=new int [ k ];
	for ( j =0; j<k; j++) //Цикл по переменной j для поиска номера j + 1 минимального элемента
		{
		kvo=0;
		for ( i =0; i<n; i++) //Перебираем все элементы массива.
		{
			//Цикл по переменной p проверяет, содержится ли номер i в массиве nmin.
			pr=false;
			for ( p=0;p<j; p++)
				if ( i==nmin [ p ] ) pr=true;
			if ( ! pr ) //Если не содержится, то количество элементов увеличить на 1.
			{
				kvo++;
				//Если kvo=1, то найден первый элемент, который не содержится в массиве
				//nmin, его номер объявляем номером минимального элемента массива
				if ( kvo==1) nmin_temp= i;
				else
					//Если kvo>1, сравниваем текущий элемент x[i] с минимальным.
					if ( x [ i ]<x [ nmin_temp ] ) nmin_temp= i;
			}
		}
	nmin [ j ]=nmin_temp; //Номер очередного минимального элемента записываем в массив.
	}
	for ( j =0; j<k; j++) //Вывод номеров и значений k минимальных элементов массива.
		cout<<" nmin1 = "<<nmin [ j ]<<" \ tmin1 = "<<x [ nmin [ j ]]<< endl;
	return 0;
}

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

Блок-схема алгоритма поиска k минимальных элементов в массиве x.


увеличить изображение

Рис. 5.10.  Блок-схема алгоритма поиска k минимальных элементов в массиве x.

#include <iostream>
using namespace std;
//Функция проверяет, содержится ли число i в массиве x из n элементов.
//Функция возвращает true, если содержится, и false, если не содержится.
bool proverka ( int i, int *x, int n )
{
	bool pr;
	int p;
	pr=false;
	for ( p=0;p<n; p++)
	if ( i==x [ p ] ) pr=true;
	return pr;
}
int main ( int arg c, char **argv )
{
	int j, i, n, *nmin, k, kvo, nmin_temp;
	double *x;
	cout<<" n = "; cin>>n;
	x=new double [ n ];
	cout<<"Введите элементы массива Х\n ";
	for ( i =0; i<n; i++)
		cin>>x [ i ];
	cout<<"Введите количество минимумов\n "; cin>>k;
	nmin=new int [ k ];
	for ( j =0; j<k; j++) //Цикл по переменной j для поиска номера j + 1 минимального элемента
	{
		kvo=0;
		for ( i =0; i<n; i++) //Перебираем все элементы массива.
		{
			//Вызов функции proverka, определяем, содержится ли число i в массиве nmin из j элементов
			if ( ! proverka ( i, nmin, j ) )
			{
				kvo++;
				if ( kvo==1) nmin_temp= i;
				else
					if ( x [ i ]<x [ nmin_temp ] ) nmin_temp= i;
			}
		}
		nmin [ j ]=nmin_temp;
	}
	for ( j =0; j<k; j++) //Вывод номеров и значений k минимальных элементов массива.
		cout<<" nmin1 = "<<nmin [ j ]<<" \ tmin1 = "<<x [ nmin [ j ]]<< endl;
	return 0;
}

Авторы настоятельно рекомендуют читателю разобрать все версии решения задачи 5.4.

Задача 5.5. Поменять местами максимальный и минимальный элементы в массиве X.

Алгоритм решения задачи можно разбить на следующие этапы.

  1. Ввод массива.
  2. Поиск номеров максимального (nmax) и минимального (nmin) элементов массива.
  3. Обмен элементов местами. Не получится записать "в лоб" (X[nmax]=X [nmin]; X[nmin]=X[nmax];). При таком присваивании мы сразу же теряем максимальный элемент. Поэтому нам понадобится временная (буферная) переменная temp. Обмен элементов местами должен быть таким: temp=X[nmax]; X[nmax]=X[nmin]; X[nmin]=temp;

Далее приведён текст программы с комментариями.

#include <iostream>
using namespace std;
int main ( int arg c, char **argv )
{
	int i,N, nmax, nmin;
	float temp;
	cout<<" N = "; cin>>N;
	float X [N ];
	cout<<"Введите элементы массива Х\n ";
	for ( i =0; i<N; i++)
		cin>>X [ i ];
	//Поиск номеров максимального и минимального элементов массива.
	for (nmax=nmin=0, i =1; i<N; i++)
	{
		if (X [ i ]<X [ nmin ] ) nmin= i;
		if (X [ i ]>X [ nmax ] ) nmax= i;
	}
	//Обмен максимального и минимального элементов местами.
	temp=X [ nmax ]; X [ nmax]=X [ nmin ]; X [ nmin ]=temp;
	cout<<"Преобразованный массив Х\n "; //Вывод преобразованного массива.
	for ( i =0; i<N; i++)
		cout<<X [ i ]<<" ";
	cout<<endl;
	return 0;
}

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

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

Алгоритм решения задачи состоит из следующих этапов:

  1. Ввод массива.
  2. Поиск номеров максимального (nmax) и минимального (nmin) элементов массива.
  3. В цикле перебираем все элементы массива, расположенные между максимальным и минимальным элементами. Если текущий элемент является простым числом, то необходимо увеличить количество простых чисел на 1, и умножить на значение элемента массива.
  4. Вычислить .

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

Текст программы с комментариями приведён ниже.

#include <iostream>
#include <math.h>
using namespace std;
bool prostoe ( int N)
{
	int i;
	bool pr;
	if (N<2) pr=false;
	else
	for ( pr=true, i =2; i<=N/ 2; i++)
	if (N%i ==0)
	{
		pr=false;
		break;
	}
	return pr;
}
int main ( int arg c, char **argv )
{
	int i, k, n, nmax, nmin, p, *x;
	cout<<" n = "; cin>>n; //Ввод количества элементов в массиве.
	x=new int [ n ]; //Выделяем память для динамического массива x.
	cout<<"Введите элементы массива X"; //Ввод элементов массива.
	for ( i =0; i<n; i++)
		cin>>x [ i ];
	//Поиск номеров максимального и минимального элементов в массиве.
	for (nmax=nmin= i =0; i<n; i++)
	{
		if ( x [ i ]<x [ nmin ] ) nmin= i;
		if ( x [ i ]>x [ nmax ] ) nmax= i;
	}
	if ( nmin<nmax )
		for ( p=1,k=0, i=nmin+1; i<nmax; i++)
		//Обратите особое внимание на использование в следующей строке фигурной скобки
		//(составного оператора). В цикле всего один оператор!!! При этом, при отсутствии
		//составного оператора, программа начинает считать с ошибками!!!
		{
			//Проверяем, является ли очередной элемент массива простым числом.
			if ( prostoe ( x [ i ] ) ) //Если x[i] — простое число.
			{
			//Домножаем y[i] на p, а также увеличиваем счётчик количества простых чисел в массиве.
			k++;p*=x [ i ];
			}
		}
	else
			for ( p=1,k=0, i=nmax+1; i<nmin; i++)
			//Проверяем, является ли очередной элемент массива простым числом.
			if ( prostoe ( x [ i ] ) ) //Если x[i] — простое число.
			{//Домножаем y[i] на p, а также увеличиваем счётчик количества простых чисел в массиве.
			k++;p*=x [ i ];
			}
		//Если в массиве были простые числа, выводим среднее геометрическое этих чисел на экран
		if ( k>0)
			cout<<" SG "<<pow ( p, 1./ k )<<endl;
		//Иначе выводим сообщение о том, что в массиве нет простых чисел.
		else cout<<"Нет простых чисел в массиве"<<endl;
	return 0;
}

5.4.5 Удаление элемента из массива

Для удаления элемента с индексом из массива , состоящего из элементов, нужно записать ()-й элемент на место элемента -й — на место ()-го и т.д., ()-й — на место ()-го. После удаления количество элементов в массиве уменьшилось на 1 (рис. 5.11).

Алгоритм удаления элемента из массива


Рис. 5.11.  Алгоритм удаления элемента из массива

Фрагмент программы на С++:

cout<<" \n m = "; cin>>m; //Ввод номера элемента, подлежащего удалению.
for ( i=m; i<n-1;X [ i ]=X [ i +1 ], i++); //Удаление m-го элемента.
n--;
for ( i =0; i<n-1; i++)cout<<X [ i ]<<" \t "; //Вывод изменённого массива.

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

Задача 5.7. Удалить из массива x[20] все элементы с пятого по десятый.

При решении задач, связанных с удалением подряд идущих элементов, следует понимать, что после удаления очередного элемента следующий переместился на место удалённого. Поэтому далее нужно будет удалять элемент с тем же самым номером. В нашем случае подлежит удалению 6 элементов с пятого по десятый. Однако, реально надо будет 6 раз удалить элемент с номером 5. Блок-схема алгоритма представлена на рис 5.12, текст программы приведён далее.

#include <iostream>
#include <math.h>
using namespace std;
int main ( int arg c, char **argv )
{
	int i, j, n=20;
	float x [ n ]; //Выделяем память для динамического массива x.
	cout<<"Введите элементы массива X \n "; //Ввод элементов массива.
	for ( i =0; i<n; i++)
		cin>>x [ i ];
	for ( j =1; j <=6; j++) //Шесть раз повторяем алгоритм удаления элемента с индексом 5.
		for ( i =5; i<n-j; i++) //Удаление элемента с индексом 5.
			x [ i ]=x [ i + 1 ];
	cout<<"Преобразованный массив X \n "; //Вывод элементов массива.
	for ( i =0; i<n-6; i++)
		cout<<x [ i ]<<" \t ";
	cout<<endl;
	return 0;
}

Алгоритм решения задачи 5.7


Рис. 5.12.  Алгоритм решения задачи 5.7

Задача 5.8. Удалить из массива X[n] все положительные элементы.

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

Далее приведён текст программы решения задачи 5.8.

#include <iostream>
#include <math.h>
using namespace std;
int main ( int arg c, char **argv )
{
	int i, j, n;
	cout<<" n = "; cin>>n;
	float x [ n ]; //Выделяем память для динамического массива x.
	cout<<"Введите элементы массива X \n "; //Ввод элементов массива.
	for ( i =0; i<n; i++)
		cin>>x [ i ];
	for ( i =0; i<n; )
		if ( x [ i ] >0) //Если текущий элемент положителен,
		{ //то удаляем элемент с индексом i.
			for ( j= i; j<n-1; j++)
			x [ j ]=x [ j + 1 ];
			n--;
		}
		else i ++; //иначе — переходим к следующему элементу массива.
	cout<<"Преобразованный массив X \n "; //Вывод элементов массива.
	for ( i =0; i<n; i++)
		cout<<x [ i ]<<" \t ";
	cout<<endl;
	return 0;
}

Задача 5.9. Удалить из массива все отрицательные элементы, расположенные между максимальным и минимальным элементами массива X[n].

Решение этой задачи можно разделить на следующие этапы:

  1. Ввод массива.
  2. Поиск номеров максимального (nmax) и минимального (nmin) элементов массива.
  3. Определение меньшего (a) и большего (b) из чисел nmax и nmin.
  4. Далее, необходимо перебрать все элементы массива, расположенные между числами с номерами a и b. Если число окажется отрицательным, то его необходимо удалить. Однако на этом этапе нужно учитывать тонкий момент. Если просто организовать цикл от a+1 до b-1, то при удалении элемента изменяется количество элементов, расположенных между a и b, и номер последнего удаляемого элемента. Это может привести к тому, что не всегда корректно будут удаляться отрицательные элементы, расположенные между a и b. Поэтому этот цикл для удаления организован несколько иначе.

Текст программы:

#include <iostream>
#include <math.h>
using namespace std;
int main ( int arg c, char **argv )
{
	int i, j, k, n, nmax, nmin, *x, a, b;
	cout<<" n = "; cin>>n; //Ввод количества элементов в массиве.
	x=new int [ n ]; //Выделяем память для динамического массива x.
	cout<<"Введите элементы массива X \n "; //Ввод элементов массива.
	for ( i =0; i<n; i++)
		cin>>x [ i ];
	//Поиск номеров максимального и минимального элементов в массиве.
	for (nmax=nmin= i =0; i<n; i++)
	{
		if ( x [ i ]<x [ nmin ] ) nmin= i;
		if ( x [ i ]>x [ nmax ] ) nmax= i;
	}
	//Проверяем, что раньше расположено, минимум или максимум
	if ( nmin<nmax )
	{
		a=nmin;
		b=nmax;
	}
	else
	{
		a=nmax;
		b=nmin;
	}
	//Перебираем все элементы, расположенные между максимумом и минимумом
	for ( i=a+1,k=1;k<=b-a-1;k++)
		if ( x [ i ] <0) //Проверяем, является ли очередной элемент массива отрицательным.
			{//Если текущий элемент массива является отрицательным числом, удаляем его
			for ( j= i; j<n-1; j++)
				x [ j ]=x [ j + 1 ];
			n--;
		}
		else i ++; //Если x[i]>=0, переходим к следующему элементу.
	cout<<"Преобразованный массив X\n ";
	for ( i =0; i<n; i++)
		cout<<x [ i ]<<" \t ";
	cout<<endl;
	return 0;
}

В качестве тестового можно использовать следующий массив: 34, 4, -7, -8, -10, 7, -100, -200, -300, 1. Здесь приведённая выше программа работает корректно, а вариант

for ( i=a+1; i<b; )
	if ( x [ i ] <0)
	{
		for ( j= i; j<n-1; j++)
			x [ j ]=x [ j + 1 ];
		n--;
	}
	else i ++;

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

Задача 5.10. В массиве X[n] найти группу наибольшей длины, которая состоит из знакочередующихся чисел.

Если будут вычислены следующие значения:

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

Вначале количество элементов в знакочередующейся группе равно 1. Дело в том, что если мы встретим первую пару знакочередующихся элементов, то количество их в группе сразу станет равным 2. Однако все последующие пары элементов будут увеличивать k на 1. И чтобы не решать проблему построения последовательности значений k 0,2,3,4,5,..., первоначальное значение k примем равным 1. Когда будем встречать очередную пару подряд идущих соседних элементов, то k необходимо будет увеличить на 1.

Алгоритм поиска очередной группы состоит в следующем: попарно () перебираем все элементы массива (параметр цикла изменяется от 0 до n - 2).

Если произведение соседних элементов отрицательно (), то это означает, что они имеют разные знаки и являются элементами группы. В этом случае количество () элементов в группе увеличиваем на 1 (k++). Если же произведение соседних элементов положительно (), то эти элементы не являются членами группы. В этом случае возможны два варианта:

  1. Если , то только что закончилась группа, в этом случае kon=i -номер последнего элемента в группе, — количество элементов в только что закончившейся группе.
  2. Если , то это просто очередная пара незнакочередующихся элементов.

После того, как закончилась очередная группа знакочередующихся элементов, необходимо количество групп (kgr) увеличить на 1 (kgr++). Если это первая группа (kgr=1) знакочередующихся элементов, то в переменную max записываем длину этой группы (max=k)1), а в переменную kon_max номер последнего элемента группы (kon_max=i). Если это не первая группа (kgr=1), то сравниваем max и длину текущей группы (k). Если k>max, то в переменную max записываем длину этой группы (max=k), а в переменную kon_max номер последнего элемента группы (kon_max=i).

После этого в переменную k опять записываем 1 для формирования новой группы элементов.

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

#include <iostream>
using namespace std;
int main ( int arg c, char **argv )
{ float *x;
	int i, k, n, max, kgr, kon_max;
	cout<<" n = "; cin>>n; //Ввод размера массива.
	x=new float [ n ]; //Выделение памяти для массива.
	cout<<"Введите массив x\n "; //Ввод элементов массива.
	for ( i =0; i<n; i++)
		cin>>x [ i ];
	//Попарно перебираем элементы массива. Количество знакочередующихся
	//групп в массиве kgr=0, количество элементов в текущей группе — 1.
	for ( kgr= i =0,k=1; i<n-1; i++)
		//Если соседние элементы имеют разные знаки, то количество (k)
		//элементов в группе увеличиваем на 1.
		if ( x [ i ] * x [ i +1]<0) k++;
		else
			if ( k>1) //Если k>1, то только что закончилась группа, i — номер последнего элемента
			{//в группе, k — количество элементов в группе. Увеличиваем kgr на 1.
				kgr++;
				if ( kgr==1) //Если это первая группа (kgr=1) знакочередующихся элементов,
				{
					max=k; //то max — длина группы (max=k),
					kon_max= i; //kon_max — номер последнего элемента группы.
				}
				else //это не первая группа (kgr 6= 1), сравниваем max и длину текущей группы.
					if ( k>max) //Если k>max,
					{
						max=k; //max — длина группы,
						kon_max= i; //kon_max — номер последнего элемента группы.
					}
				k=1; //В переменную k записываем 1 для формирования новой группы элементов.
			}
		if ( k>1) //Если в конце массива была группа.
		{
			kgr++; //Количество групп увеличиваем на 1.
			if ( kgr==1) //Если это первая группа,
			{
				max=k; //то max — длина группы,
				kon_max=n-1; //группа закончилась на последнем элементе массива.
			}
			else
				if ( k>max) //Если длина очередной группы больше max.
				{
					max=k; //то в max записываем длину последней группы,
					kon_max=n-1; //группа закончилась на последнем элементе массива.
				}
		}
		if ( kgr >0) //Если знакочередующиеся группы были,
		{ //то выводим информацию о группе наибольшей длины,
			cout<<"В массиве "<<kgr<<" групп знакочередующихся элементов\n ";
			cout<<"Группа максимальной длины начинается с элемента Номер
				"<<kon_max-max+1<<", её длина "<<max<<",номер последнего элемента группы
				" <<kon_max<<endl;
			for ( i=kon_max-max+1; i<=kon_max; i++) //а также саму группу.
				cout<<x [ i ]<<" ";
			cout<<endl;
		}
		else //Если знакочередующихся групп не было, то выводим сообщение об этом.
			cout<<"В массиве нет групп знакочередующихся элементов\n ";
		return 0;
}

5.4.6 Сортировка элементов в массиве

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

и в порядке убывания, если

Существует большое количество алгоритмов сортировки, но все они базируются на трёх основных:

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

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

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

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

5.4.6.1 Сортировка методом "пузырька"

Сортировка пузырьковым методом является наиболее известной. Её популярность объясняется запоминающимся названием, которое происходит из-за подобия процессу движения пузырьков в резервуаре с водой, когда каждый пузырёк находит свой собственный уровень, и простотой алгоритма. Сортировка методом "пузырька" использует метод обменной сортировки и основана на выполнении в цикле операций сравнения и при необходимости обмена соседних элементов. Рассмотрим алгоритм пузырьковой сортировки более подробно.

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

В табл. 5.4 представлен процесс упорядочивания элементов в массиве.

Таблица 5.4. Процесс упорядочивания элементов
Номер элемента01234
Исходный массив73542
Первый просмотр35427
Второй просмотр34257
Третий просмотр32457
Четвёртый просмотр23457

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

Сортировка массива пузырьковым методом


Рис. 5.13.  Сортировка массива пузырьковым методом

Обратите внимание на то, что для перестановки элементов (рис. 5.13, блок 4) используется буферная переменная , в которой временно хранится значение элемента, подлежащего замене. Текст программы, сортирующей элементы в массиве по возрастанию методом "пузырька", приведён далее.

#include <iostream>
using namespace std;
int main ( )
{
	int n, i, b, j;
	cout<<" n = "; cin>>n;
	float y [ n ];
	for ( i =0; i<n; i++) //Ввод массива.
	{
		cout<<" \n Y [ "<<i<<" ]= ";
		cin>>y [ i ];
	}
	for ( j =1; j<n; j++) //Упорядочивание элементов в массиве по возрастанию их значений.
		for ( i =0; i<n-j; i++)
		if ( y [ i ]>y [ i +1 ]) //Если текущий элемент больше следующего
		{
			b=y [ i ]; //Сохранить значение текущего элемента
			y [ i ]=y [ i + 1 ]; //Заменить текущий элемент следующим
			y [ i +1]=b; //Заменить следующий элемент на сохранённый в b
		}
	for ( i =0; i<n; i++) cout<<y [ i ]<<" \t "; //Вывод упорядоченного массива
	return 0;
}

Для перестановки элементов в массиве по убыванию их значений необходимо в программе и блок-схеме при сравнении элементов массива заменить знак ">" на "<".

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

Вариант 1.

#include <iostream>
using namespace std;
int main ( int arg c, char **argv )
{
	int n, i, b, j;
	bool pr;
	cout<<" n = "; cin>>n;
	float y [ n ];
	for ( i =0; i<n; i++) //Ввод массива.
	{
		cout<<" \n Y [ "<<i<<" ]= ";
		cin>>y [ i ];
	}
	for ( j =1; j<n; j++) //Упорядочивание элементов массива по убыванию их значений.
	{
		for ( pr=false, i =0; i<n-j; i++) //Предполагаем, что массив уже отсортирован
		// ( pr=false ) .
		if ( y [ i ]<y [ i +1 ]) //Если текущий элемент меньше следующего
		{
			b=y [ i ]; //Сохранить значение текущего элемента
			y [ i ]=y [ i + 1 ]; //Заменить текущий элемент следующим
			y [ i +1]=b; //Заменить следующий элемент текущим
			pr=true; //Если элемент менялись местами, массив ещё не отсортирован (pr=true);
		}
		cout<<" j = "<<j<<endl;
		//Если на j-м шаге соседние элементы не менялись, то массив уже отсортирован,
		if ( ! pr ) break; //повторять смысла нет;
	}
	for ( i =0; i<n; i++) cout<<y [ i ]<<" \t "; //Вывод упорядоченного массива
	return 0;
}

Вариант 2.

#include <iostream>
using namespace std;
int main ( int arg c, char **argv )
{
	int n, i, b, j;
	bool pr=true;
	cout<<" n = "; cin>>n;
	float y [ n ];
	for ( i =0; i<n; i++) //Ввод массива.
	{
		cout<<" \n Y [ "<<i<<" ]= ";
		cin>>y [ i ];
	}
	for ( j =1; pr; j++) //Упорядочивание элементов массива по убыванию их значений.
	{ //Вход в цикл, если массив не отсортирован (pr=true).
		for ( pr=false, i =0; i<n-j; i++) //Предполагаем, что массив уже отсортирован
		// ( pr=false ) .
			if ( y [ i ]<y [ i +1 ]) //Если текущий элемент меньше следующего
			{
				b=y [ i ]; //Сохранить значение текущего элемента
				y [ i ]=y [ i + 1 ]; //Заменить текущий элемент следующим
				y [ i +1]=b; //Заменить следующий элемент текущим
				pr=true; //Элементы менялись местами, массив ещё не отсортирован ( pr=true )
			}
	}
	for ( i =0; i<n; i++) cout<<y [ i ]<<" \t "; //Вывод упорядоченного массива
	return 0;
}
5.4.6.2 Сортировка выбором

Алгоритм сортировки выбором приведён в виде блок-схемы на рис. 5.14. Идея алгоритма заключается в следующем. В массиве , состоящем из элементов, ищем самый большой элемент (блоки 2–5) и меняем его местами с последним элементом (блок 7). Повторяем алгоритм поиска максимального элемента, но последний -й элемент не рассматриваем, так как он уже занял свою позицию.

Сортировка массива выбором наибольшего элемента


Рис. 5.14.  Сортировка массива выбором наибольшего элемента

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

for ( j =1; j<n; b=y [ n-j ], y [ n-j ]=y [ nom ], y [ nom]=b, j++)
	for (max=y [ 0 ], nom=0, i =1; i<=n-j; i++)
		if ( y [ i ]>max) {max=y [ i ]; nom= i; }

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

5.4.6.3 Сортировка вставкой

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

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

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

Сортировка массива вставкой


Рис. 5.15.  Сортировка массива вставкой

Организуем цикл для просмотра всех элементов массива, начиная с первого (блок 1). Сохраним значение текущего –го элемента во вспомогательной переменной b, так как оно может быть потеряно при сдвиге элементов (блок 2), и присвоим переменной значение индекса предыдущего –го элемента массива (блок 3). Далее движемся по массиву влево в поисках элемента, меньшего чем текущий, и, пока он не найден, сдвигаем элементы вправо на одну позицию. Для этого организуем цикл (блок 4), который прекратиться, как только будет найден элемент меньше текущего. Если такого элемента в массиве не найдётся и переменная станет равной (-1), то это будет означать, что достигнута левая граница массива, и текущий элемент необходимо установить в первую позицию. Смещение элементов массива вправо на одну позицию выполняется в блоке 5, а изменение счётчика в блоке 6. Блок 7 выполняет вставку текущего элемента в соответствующую позицию. Далее приведён фрагмент программы, реализующей сортировку массива методом вставки.

for ( i =1; i<n; y [ j +1]=b, i++)
	for ( b=y [ i ], j=i -1;( j >-1 && b<y [ j ] ); y [ j +1]=y [ j ], j --);

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

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

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

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

  1. Найти номер первого числа в массиве, которое a_k ‎ b.
  2. Все элементы массива a, начиная от до -го, сдвинуть на один вправо1).
  3. На освободившееся место с номером записать число .

Текст программы с комментариями приведён ниже.

#include <iostream>
2 using namespace std;
	int main ( int arg c, char **argv )
4 {
	int i, k, n;
6 	float b;
	cout<<" n = "; cin>>n; //Ввод размера исходного массива.
8 	float a [ n + 1 ]; //Выделение памяти с учётом предстоящей вставки одного числа в массив.
	cout<<"Введите массив a \n "; //Ввод исходного упорядоченного по убыванию массива.
10 	for ( i =0; i<n; i++)
		cin>>a [ i ];
12 	cout<<"Введите число b = "; cin>>b; //Ввод вставляемого в массив числа b .
	//Если число b меньше всех элементов массива, записываем b в последний элемент массива.
14 	if ( a [ n-1]>=b ) a [ n ]=b;
	else //Иначе
16 	{
		for ( i =0; i<n; i++) //Ищем первое число, меньшее b .
18 		if ( a [ i ]<=b )
		{
20 			k= i; //Запоминаем его номер в переменной k .
			break;
22 		}
		for ( i=n-1; i>=k; i --) //Все элементы массива от n -1-го до k-го сдвигаем на один вправо.
24 			a [ i +1]=a [ i ];
		a [ k ]=b; //Вставляем число b в массив.
26 	}
	cout<<"Преобразованный массив a \n ";
28 	for ( i =0; i<=n; i++)
	cout<<a [ i ]<<" \t ";
30 	return 0;
}

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

Задача 5.12. Проверить, является ли массив упорядоченным по возрастанию.

Для проверки упорядоченности по возрастанию a[n]2) можно поступить следующим образом. Предположим, что массив упорядочен (pr=true). Если хотя бы для одной пары соседних элементов выполняется условие , то массив не упорядочен по возрастанию (pr=false). Текст программы с комментариями приведён ниже. Читателю предлагается преобразовать программу таким образом, чтобы осуществлялась проверка, упорядочен ли массив по убыванию.

#include <iostream> namespace std;
int main ( int arg c, char **argv )
{
	int i, n;
	bool pr;
	cout<<" n = "; cin>>n; //Ввод размера исходного массива.
	float *a=new float [ n ]; //Выделение памяти для массива.
	cout<<"Введите массив a \n "; //Ввод исходного массива.
	for ( i =0; i<n; i++)
		cin>>a [ i ];
	//Предполагаем, что массив упорядочен (pr=true), перебираем все пары соседних значений
	//(i — номер пары), при i равном n - 2 будем сравнивать последнюю пару a[n-2] и a[n-1].
	for ( pr=true, i =0; i<n-1; i++)
	//Если для очередной пары соседних элементов выяснилось, что предыдущий элемент больше
	//последующего, то массив неупорядочен по возрастанию (pr=false), остальные пары соседних
	//значений, можно не проверять (оператор break)
		if ( a [ i ]>a [ i +1 ]) { pr=false; break; }
	if ( pr ) cout<<"Массив упорядочен по возрастанию";
	else cout<<"Массив не упорядочен по возрастанию";
	return 0;
}

5.5 Указатели на функции

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

type (*name_f ) ( type1, type2, type3,...)

Здесь

В качестве примера рассмотрим решение широко известной математической задачи.

Задача 5.13. Вычислить методами Гаусса и Чебышёва.

Кратко напомним читателю методы численного интегрирования.

Метод Гаусса состоит в следующем. Определённый интеграл непрерывной функции на интервале от -1 до 1 можно заменить суммой и вычислить по формуле — точки из интервала [-1, 1], — рассчитываемые коэффициенты. Методика определения представлена в [3]. Для практического использования значения коэффициентов при представлены в табл. 5.5.

Таблица 5.5. : Значения коэффициентов в квадратурной формуле Гаусса
nМассив tМассив A
2-0.57735027, 0.577350271,1
3-0.77459667, 0, 0.774596675/9, 8/9, 5/9
4-0.86113631, -0.33998104, 0.33998104,0.861136310.34785484, 0.65214516, 0.65214516, 0.34785484
5-0.90617985, -0.53846931, 0, 0.53846931,0.906179850.23692688, 0.47862868, 0.568888889,0.47862868, 0.23692688
6-0.93246951, -0.66120939, -0.23861919, 0.23861919, 0.66120939, 0.932469510.17132450, 0.36076158, 0.46791394, 0.46791394, 0.36076158, 0.17132450
7-0.94910791, -0.74153119, -0.40584515, 0, 0.40584515, 0.74153119, 0.949107910.12948496, 0.27970540, 0.38183006, 0.41795918, 0.38183006, 0.27970540, 0.12948496
8-0.96028986, -0.79666648, -0.52553242, -0.18343464, 0.18343464, 0.52553242, 0.79666648, 0.960289860.10122854, 0.22238104, 0.31370664, 0.36268378, 0.36268378, 0.31370664, 0.22238104, 0.10122854

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

При использовании квадратурной формулы Чебышёва, определённый интеграл непрерывной функции на интервале от -1 до 1 записывается в виде следующей формулы — точки из интервала [-1, 1]. Формула Чебышёва для вычисления интеграла на интервале от до может быть записана так Методика определения представлена в [3]. Рассмотренные формулы имеют смысл при , коэффициенты представлены в табл. 5.6.

Таблица 5.6. Значения коэффициентов в квадратурной формуле Чебышёва
nМассив t
2-0.577350, 0.577350
3-0.707107, 0, -0.707107
4-0.794654, -0.187592, 0.187592, 0.794654
5-0.832498, -0.374541, 0, 0.374541, 0.832498
6-0.866247, -0.422519, -0.266635, 0.266635, 0.422519, 0.866247
7-0.883862, -0.529657, -0.323912, 0, 0.323912, 0.529657, 0.883862
9-0.911589, -0.601019, -0.528762, -0.167906, 0, 0.167906, 0.528762, 0.601019, 0.911589

Осталось написать функции вычисления определённого интеграла методами Гаусса и Чебышёва. Далее приведены тексты функций и функция main(). В качестве тестовых использовались интегралы .

#include <iostream>
#include <math.h>
using namespace std;
//Функция вычисления определённого интеграла методом Чебышёва.
//(a, b) — интервал интегрирования, *fn — указатель на функцию типа double f (double).
double int_chebishev ( double a, double b,
double (*fn ) ( double ) )
{
	int i, n=9;
	double s,
	t [ 9 ]= {-0.911589, -0.601019, -0.528762, -0.167906, 0, 0.167906, 0.528762,
		0.601019, 0.911589 };
	for ( s= i =0; i<n; i++)
		s+=fn ( ( b+a ) /2+(b-a ) /2*t [ i ] );
	s *=(b-a ) /n;
	return s;
}
//Функция вычисления определённого интеграла методом Гаусса.
//(a, b) — интервал интегрирования, *fn — указатель на функцию типа double f (double)
double int_gauss ( double a, double b, double (*fn ) ( double ) )
{
	int i, n=8;
	double s,
	t [8]= { -0.96028986, -0.79666648, -0.52553242, -0.18343464, 0.18343464,
		0.52553242, 0.79666648, 0.96028986 },
	A[8]= { 0.10122854, 0.22238104, 0.31370664, 0.36268378, 0.36268378,
		0.31370664, 0.22238104, 0.10122854 };
	for ( s= i =0; i<n; i++)
		s+=A [ i ] *fn ( ( b+a ) /2+(b-a ) /2* t [ i ] );
	s *=(b-a ) / 2;
	reurn s;
}
//Функции f1 и f2 типа double f(double), указатели на которые будут передаваться
//в int_gauss и int_chebishev.
double f1 ( double y )
{
	return sin(y) *sin(y) *sin(y) *sin( );
}
double f2 ( double y )
{
	return pow ( 2*y -1, 0.5 );
}
int main ( int arg c, char **argv )
{
	double a, b;
	cout<<"Интеграл sin(x)^4 = \n ";
	cout<<"Введите интервал интегрирования\n ";
	cin>>a>>b;
	//Вызов функции int_gauss(a, b, f1), f1 — имя функции, интеграл от которой надо посчитать.
	cout<<"Метод Гаусса:"<<int_gauss ( a, b, f1 )<<endl;
	//Вызов функции int_chebishev(a, b, f1),
	//f1 — имя функции, интеграл от которой надо посчитать.
	cout<<"Метод Чебышёва:"<<int_chebishev( a, b, f1 )<<endl;
	cout<<"Интеграл sqrt ( 2*x-1 ) =\n ";
	cout<<"Введите интервалы интегрирования\n ";
	cin>>a>>b;
	//Вызов функции int_gauss(a, b, f2), f2 — имя функции, интеграл от которой надо посчитать.
	cout<<"Метод Гаусса:"<<int_gauss ( a, b, f2 )<<endl;
	//Вызов функции int_chebishev(a, b, f2),
	//f2 — имя функции, интеграл от которой надо посчитать.
	cout<<"Метод Чебышёва:"<<int_chebishev ( a, b, f2 )<<endl;
	return 0;
}

Результаты работы программы приведены ниже

Интеграл sin(x)^4=
Введите интервалы интегрирования
0 2
Метод Гаусса:0.970118
Метод Чебышёва:0.970082
Интеграл sqrt(2*x-1)=
Введите интервалы интегрирования
5 13
Метод Гаусса:32.6667
Метод Чебышёва:32.6667

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

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

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

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

Назовём функцию udal. Её входными параметрами будут:

Функция возвращает:

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

Заголовок (прототип) функции udal может быть таким:

int udal( float *x, int k, int n )

Здесь x — массив, k — номер удаляемого элемента, n — размер массива.

Весь текст функции можно записать так:

int udal ( float *x, int k, int n )
{
	int i;
	if ( k>n-1) return n;
	else
	{
		for ( i=k; i<n-1; i++)
			x [ i ]=x [ i + 1 ];
		n--;
		return n;
	}
}

Ниже приведён весь текст программы удаления положительных элементов из массива x c использованием функции udal и комментарии к нему.

#include <iostream>
#include <math.h>
using namespace std;
int udal ( float *x, int k, int n )
{
	int i;
		//Если номер удаляемого элемента больше номера последнего элемента,
		//то удалять нечего, в этом случае возвращается неизменённый размер массива
		if ( k>n-1) return n;
		else
		{
			for ( i=k; i<n-1; i++) //Удаляем элемент с номером k.
				x [ i ]=x [ i + 1 ];
			n--;
			return n; //Возвращаем изменённый размер массива.
	}
	}
	int main ( int arg c, char **argv )
	{
	int i, n;
	cout<<" n = "; cin>>n;
	float x [ n ]; //Выделяем память для динамического массива x.
	cout<<"Введите элементы массива X \n "; //Ввод элементов массива.
	for ( i =0; i<n; i++)
		cin>>x [ i ];
	for ( i =0; i<n; )
		if ( x [ i ] >0)
	//Если текущий элемент положителен, то для удаления элемента с индексом i вызываем
	//функцию udal, которая изменяет элементы, хранящиеся по адресу x,
			n=udal ( x, i, n ); //и возвращает размер массива.
		else i ++; //иначе (x[i]<=0) — переходим к следующему элементу массива.
	cout<<"Преобразованный массив X \n "; //Вывод элементов массива после удаления.
	for ( i =0; i<n; i++)
		cout<<x [ i ]<<" \t ";
	cout<<endl;
	return 0;
}

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

void udal ( float *x, int k, int *n )
{
	int i;
	for ( i=k; i <*n-1; i++)
		x [ i ]=x [ i + 1 ];
	if ( k<*n ) --*n;
}

В этом случае изменится и обращение к udal в функции main.

Ниже приведён модифицированный текст программы удаления положительных элементов из массива x c использованием функции udal(float *x, int k, int *n) и комментарии к нему.

#include <iostream>
#include <math.h>
using namespace std;
void udal ( float *x, int k, int*n )
{
	int i;
	//Если номер удаляемого элемента больше номера последнего элемента, то удалять нечего,
	//в этом случае возвращается неизменённый размер массива. Удаляем элемент с номером k.
	for ( i=k; i <-n-1; i++)
		x [ i ]=x [ i + 1 ];
	//Уменьшаем на 1 значение, хранящееся по адресу n.
	//Обратите внимание, что надо писать именно --*n, *n-- — НЕПРАВИЛЬНО!!!!!!!!!!!!!!!
	if ( k<*n ) --*n;
}
int main ( int arg c, char **argv )
{
	int i, n;
	cout<<" n = "; cin>>n;
	float x [ n ]; //Выделяем память для динамического массива x.
	cout<<"Введите элементы массива X \n "; //Ввод элементов массива.
	for ( i =0; i<n; i++)
		cin>>x [ i ];
	for ( i =0; i<n; )
	if ( x [ i ] >0) //Если текущий элемент положителен, то удаление элемента с индексом i,
		//Вызываем функцию udal, которая изменяет элементы, хранящиеся по адресу x,
		//и изменяет значение переменной n.
		udal ( x, i,&n );
		else i ++; //иначе (x[i]<=0) — переходим к следующему элементу массива.
	cout<<"Преобразованный массив X \n "; //Вывод элементов массива после удаления.
	for ( i =0; i<n; i++)
		cout<<x [ i ]<<" \t ";
	cout<<endl;
	return 0;
}

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

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

Алгоритм решения этой задачи без применения функций будет очень громоздким, а текст программы малопонятным. Поэтому разобьём задачу на подзадачи:

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

Текст программы с комментариями:

#include <iostream>
using namespace std;
float sr_arifm ( int *x, int n ) //Функция вычисления среднего значения.
{
	int i; float s =0;
	for ( i =0; i<n; s+=x [ i ], i++);
	if ( n>0) return ( s /n );
	else return 0;
}
bool prostoe ( int n ) //Функция для проверки, является ли число n простым.
{
	bool pr; int i;
	for ( pr=true, i =2; i<=n / 2; i++)
	if ( n%i ==0) { pr=false; break; }
	return ( pr );
}
void ud a l ( int *x, int m, int *n ) //Функция удаления элемента из массива.
{
	int i;
	for ( i=m; i <*n-1;*( x+ i ) =*(x+ i +1), i++);
	--*n;
	realloc ( ( int *) x, *n*sizeof ( int ) );
}
void upor ( int *x, int n, bool pr=true ) //Функция сортировки массива.
{
	int i, j, b;
	if ( pr )
	{
	for ( j =1; j<=n-1; j++)
	for ( i =0; i<=n-1-j; i++)
	if ( *( x+ i ) >*(x+ i +1) )
	{
		b=*(x+ i );
		*( x+ i ) =*(x+ i +1);
		*( x+ i +1)=b;
	}
	}
	else
		for ( j =1; j<=n-1; j++)
	for ( i =0; i<=n-1-j; i++)
		if ( * ( x+ i ) <*(x+ i +1) )
		{
			b=*(x+ i );
			*( x+ i ) =*(x+ i +1);
			*( x+ i +1)=b;
		}
}
int main ( )
{
	int *a, n, i; float sr;
	cout<<" n = "; cin>>n; //Ввод размерности массива.
	a=( int *) calloc ( n, sizeof ( int ) ); //Выделение памяти.
	cout << "Введите массив A \n ";
	for ( i =0; i<n; i++) cin >>*(a+ i ); //Ввод массива.
	sr=sr_arifm ( a, n ); //Вычисление среднего арифметического.
	cout<<" sr = "<<s r<<" \n "; //Вывод среднего арифметического.
	for ( i =0; i<n; )
	{
		if ( prostoe ( * ( a+ i ) )&& *( a+ i )<sr ) //Если число простое и меньше среднего,
			udal ( a, i,&n ); //удалить его из массива,
		else i ++; //иначе, перейти к следующему элементу.
	}
	cout << "Массив A \n "; //Вывод модифицированного массива.
	for ( i =0; i<n; i++) cout <<*(a+ i )<<" \t ";
	cout<<" \n ";
	upor ( a, n ); //Сортировка массива.
	cout<<"Упорядоченный массив A \n "; //Вывод упорядоченного массива.
	for ( i =0; i<n; i++) cout <<*(a+ i )<<" \t ";
	cout<<" \n ";
	free ( a ); //Освобождение памяти.
	return 0;
}

Задача 5.15. Все положительные элементы целочисленного массива переписать в массив . В массиве упорядочить по убыванию элементы, которые расположены между наибольшим и наименьшим числами-палиндромами.

Для создания этой программы напишем следующие функции:

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

#include <iostream>
#include <stdlib .h>
#include <math.h>
using namespace std;
int kvo_razryad ( int M)
{
	long int k;
	for ( k=1;M>9;M/=10,k++);
	return k;
}
bool palindrom ( int n )
{
	int k=kvo_razryad ( n ), s, p=n;
	for ( s =0;p != 0; p/=10,k--)
		s+=(p%10)*pow ( 10, k-1);
	if ( s==n ) return true; else return false;
}
int form ( int *a, int n, int *b )
{
	int i, k;
	for ( i=k=0; i<n; i++)
		if ( a [ i ] >0)
			b [ k++]=a [ i ];
	return k;
}
void sort ( int *x, int n, int k, int p )
{
	int i, nom, j;
	int b;
	for ( i=k+1; i<p; )
	{
		nom= i;
		for ( j= i +1; j<p; j++)
			if ( x [ j ]<x [ nom ] ) nom=j;
		b=x [ p -1 ]; x [ p-1]=x [ nom ]; x [ nom]=b;
		p--;
	}
}
int main ( int arg c, char **argv )
{
	int *G, *W;
	int nmax, nmin, kp, i,N, k;
	cout<<" N = ";
	cin>>N;
	G=( int *) calloc (N, sizeof ( int ) );
	W=( int *) calloc (N, sizeof ( int ) );
	cout<<"Ввод массива G \n ";
	for ( i =0; i<N; i++)
		cin>>G[ i ];
	k=form (G,N,W);
	cout<<"Вывод массива W \n ";
	for ( i =0; i<k; i++)
	cout<<W[ i ]<<" ";
	cout<<endl;
	for ( kp= i =0; i<k; i++)
		if ( palindrom (W[ i ] ) )
		{
			kp++;
			if ( kp==1) {nmax= i; nmin= i; }
			else
			{
				if (W[ i ]<W[ nmin ] ) nmin= i;
				if (W[ i ]>W[ nmax ] ) nmax= i;
			}
		}
	if (nmax<nmin )
		sort (W, k, nmax, nmin );
	else
		sort (W, k, nmin, nmax );
	cout<<"Вывод преобразованного массива W \n ";
	for ( i =0; i<k; i++)
		cout<<W[ i ]<<" ";
	cout<<endl;
	return 0;
}

Результаты работы программы представлены ниже.

N=17
Ввод массива G
-5 -6 191 121 12 -13 14 15 -5 100 666 -666 15251 16261 16262 991 -724
Вывод массива W
191 121 12 14 15 100 666 15251 16261 16262 991
Вывод преобразованного массива W
191 121 15251 666 100 15 14 12 16261 16262 991

5.7 Задачи для самостоятельного решения

5.7.1 Основные операции при работе с массивами

Разработать программу на языке C++ для решения следующей задачи.

  1. Задан массив целых чисел . Найти
    • сумму чётных элементов массива;
    • наибольшее из отрицательных чисел массива.
    Из данного массива и некоторого массива того же типа, но другой размерности , сформировать общий массив. Выполнить сортировку полученного массива по возрастанию модулей. Удалить из массива число с номером .
  2. Задан массив вещественных чисел . Найти
    • произведение положительных элементов массива;
    • сумму отрицательных чисел, расположенных после максимального элемента массива.
    Из данного массива и некоторого массива того же типа, но другой размерности , сформировать общий массив . Преобразовать полученный массив так, чтобы все его положительные элементы стали отрицательными, и наоборот. Удалить предпоследний элемент массива.
  3. Задан массив вещественных чисел . Найти
    • произведение ненулевых элементов массива.
    • сумму чётных чисел, расположенных до минимального элемента массива.
    Из заданного массива все положительные числа переписать в массив , а отрицательные в массив . Удалить из массива первый чётный элемент.
  4. Задан массив целых чисел . Найти
    • сумму положительных чётных элементов массива;
    • количество элементов массива, расположенных после первого нулевого элемента.
    Из данного массива и некоторого массива того же типа, но другой размерности , сформировать общий массив . Удалить из полученного массива наибольший элемент.
  5. Задан массив вещественных чисел . Найти
    • сумму элементов с нечётными номерами;
    • произведение элементов массива, расположенных между первым и последним отрицательными элементами.
    Из данного массива и некоторого массива того же типа, но другой размерности , сформировать общий массив . Удалить из полученного массива наименьший элемент.
  6. Задан массив вещественных чисел X. Найти
    • сумму положительных элементов массива;
    • произведение элементов с нечётными индексами, расположенных во второй половине массива.
    Из данного массива и некоторого массива того же типа, но другой размерности , сформировать общий массив таким образом, чтобы в нём сначала располагались все отрицательные элементы, затем элементы, равные нулю, и в заключение все положительные. Удалить из массива максимальный элемент.
  7. Задан массив целых чисел . Найти
    • произведение отрицательных элементов с чётными индексами.
    • максимальный элемент среди элементов, которые кратны 3.
    Из данного массива и некоторого массива того же типа, но другой размерности , сформировать массив , состоящий только из неотрицательных значений заданных массивов. Удалить из массива первое число, кратное 17.
  8. Задан массив целых чисел . Найти
    • сумму чисел, расположенных в первой половине массива;
    • разность между значениями максимального и минимального элементов массива.
    Из данного массива сформировать новый массив , в который записать все ненулевые элементы массива . Удалить из массива последнее чётное число.
  9. Задан массив целых чисел . Найти
    • произведение элементов массива, кратных трём;
    • сумму чисел, которые расположены между минимальным и максимальными элементами массива.
    Из данного массива сформировать новый массив , в который переписать все элементы массива в обратном порядке. Удалить из массива минимальный и максимальный элементы.
  10. Задан массив целых чисел . Найти
    • сумму нечётных положительных элементов массива;
    • количество чисел, которые расположены до первого нулевого элемента в массиве.
    Записать элементы заданного массива в обратном порядке. Определить положение максимального элемента до и после преобразования. Удалить максимальный элемент.
  11. Задан массив целых чисел . Найти
    • сумму чётных элементов;
    • количество чисел, которые расположены после минимального элемента массива.
    Заменить нулевые элементы заданного массива значениями их номеров. Определить среднее арифметическое элементов массива до и после преобразования. Удалить минимальный элемент массива .
  12. Задан массив вещественных чисел . Найти
    • процент отрицательных чисел в массиве;
    • сумму первого и последнего положительных элементов.
    Записать элементы заданного массива в обратном порядке. Определить положение минимального элемента до и после преобразования. Удалить минимальный элемент.
  13. Задан массив целых чисел . Найти
    • среднее арифметическое элементов массива;
    • минимальный элемент и его индекс в первой половине массива.
    Из данного массива и некоторого массива того же типа, но другой размерности сформировать общий массив , в который переписать удвоенные положительные значения элементов исходных массивов. Удалить из массива последний чётный элемент.
  14. Задан массив целых чисел . Найти
    • сумму элементов массива, кратных 13;
    • количество чётных чисел, расположенных до максимального элемента массива.
    Сформировать массив , в который переписать квадраты отрицательных элементов исходного массива . Удалить из массива три последних чётных элемента.
  15. Задан массив целых чисел . Найти
    • количество нечётных элементов массива;
    • произведение чисел, расположенных до минимума.
    Первую половину массива переписать в массив , а вторую в массив . Найти сумму квадратов разностей элементов массивов и . Удалить из массива последнее число, кратное 5.
  16. Задан массив целых чисел . Найти
    • сумму чётных элементов во второй половине массива;
    • количество чисел расположенных между первым и последним отрицательными элементами массива.
    Из заданного массива все положительные числа переписать в массив , а отрицательные в массив . Поменять местами максимальный и минимальный элементы в массиве . Удалить третий элемент массива .
  17. Задан массив целых чисел . Найти
    • количество чётных элементов в массиве;
    • среднее геометрическое положительных элементов массива, расположенных в его первой половине.
    Все отрицательные элементы заданного массива заменить значением его максимального элемента. Удалить из массива первый нулевой элемент.
  18. Задан массив целых чисел . Найти
    • сумму модулей элементов массива;
    • номер первого нулевого элемента.
    Из данного массива и некоторого массива того же типа, но другой размерности сформировать общий массив , в который переписать положительные значения элементов исходных массивов. Удалить из массива наибольший чётный элемент.
  19. Задан массив целых чисел . Найти
    • произведение чисел, кратных 7;
    • количество чисел, которые расположены между первым и последним чётными числами.
    Из данного массива сформировать новый массив , в который переписать первые положительных элементов массива . Удалить из массива число, наименее отличающееся от среднего арифметического значения элементов массива.
  20. Задан массив целых чисел . Найти
    • произведение ненулевых элементов массива;
    • среднее арифметическое элементов массива, расположенных в его первой половине.
    Из данного массива и некоторого массива того же типа и размерности сформировать массив каждый элемент которого равен квадрату суммы соответствующих элементов массивов и . Удалить из массива наибольший и наименьший элементы.
  21. Задан массив вещественных чисел . Найти
    • произведение абсолютных значений элементов массива;
    • количество нечётных элементов массива, расположенных в его второй половине.
    Из данного массива и некоторого массива того же типа и размерности сформировать массив каждый элемент которого равен квадрату разности соответствующих элементов массивов и . Удалить из массива минимальный элемент и поменять местами первый и последний элементы.
  22. Задан массив целых чисел . Найти
    • сумму элементов массива, кратных трём;
    • произведение ненулевых элементов массива с чётными индексами.
    Сформировать массива B, в который записать последние k элементов массива . Удалить из массива максимальный нечётный элемент.
  23. Задан массив вещественных чисел . Найти
    • количество положительных элементов массива;
    • номера первого положительного и последнего отрицательного элементов массива.
    В массиве поменять местами первые и последние пять элементов. Удалить из массива элемент, наименее отличающийся от среднего арифметического.
  24. Задан массив целых чисел . Найти
    • среднее геометрическое элементов, которые кратны трём и хранятся в массиве под чётным индексом.
    • минимальный элемент среди положительных чётных элементов.
    Из данного массива и некоторого массива того же типа, но другой размерности сформировать массив , состоящий только из положительных значений заданных массивов. Удалить из массива первый чётный и последний нечётный элементы.
  25. Задан массив вещественных чисел . Найти
    • номер минимального по модулю элемента массива;
    • среднее арифметическое первых положительных элементов.
    Из данного массива и некоторого массива того же типа, но другой размерности сформировать общий массив таким образом, чтобы сначала располагались все отрицательные элементы, а затем все положительные. Удалить из массива наибольшее и наименьшее простое число.

5.7.2 Применение функций для обработки массивов.

Разработать программу на языке C++ для решения следующей задачи.

  1. Задан массив целых чисел . Все простые числа переписать в массив . Из массива удалить 5 наибольших элементов массива. Вывести на экран содержимое массива в двоичной системе.
  2. Заданы массивы целых чисел и . Все совершённые числа из этих массивов переписать в массив . В массиве найти четыре наименьших элемента массива. Результаты вывести на экран в восьмеричной системе.
  3. Заданы массивы целых чисел и Два наибольших элемента из массива и пять последних простых чисел из массива переписать в массив . Проверить содержит ли массив числа, в которых есть цифра "7".
  4. Заданы массивы целых чисел и . Три наименьших простых числа из массива и числа из массива , в которых есть цифры "1" и "9" переписать в массив . Из массива удалить все нечётные числа.
  5. Задан массив целых чисел . Шесть наибольших чисел этого массива переписать в массив . Удалить из массива все чётные числа. Вывести на экран элементы массива в восьмеричной системе счисления.
  6. Заданы массивы целых чисел и . Числа из массива , в которых нет "нулей" и составные числа из массива , переписать в массив . Найти в массиве пять наибольших нечётных чисел. Выполнить сортировку массивов и в порядке возрастания их элементов.
  7. Заданы массивы целых положительных чисел. — в двоичной системе счисления, а — в восьмеричной. Все числа из массивов и переписать в массив десятичных чисел . В массиве найти пять наибольших простых числа. Удалить из массива все составные числа.
  8. Задан массив целых положительных чисел . Все простые числа длиной не более пяти цифр переписать в массив .Удалить из массива два наибольших и три наименьших числа.
  9. Задан массив целых положительных чисел в пятеричной системе . Из массива сформировать массив десятеричных чисел . Найти сумму трёх наименьших и четырёх наибольших чисел массива .
  10. Заданы массивы целых положительных чисел . Сформировать массив из таких элементов массивов , которые в восьмеричной системе образуют возрастающую последовательность цифр. Найти пять наибольших чисел в массива .
  11. Задан массив целых положительных чисел . Все числа в которых нет цифр "1", "2" и "3" переписать в массив . Найти сумму двух наибольших и трёх наименьших простых чисел в массиве .
  12. Заданы массивы целых положительных чисел . Сформировать массив из таких элементов массивов которые состоят из одинаковых цифр. Удалить из массива наибольшее и наименьшее число. Выполнить сортировку массивов в порядке возрастания их элементов.
  13. Задан массив целых положительных чисел . Все числа, в которых нет цифры ноль, а их длина не менее трёх цифр переписать в массив . Поменять местами наибольшее составное число и наименьшее простое число в массиве .
  14. Задан массив целых чисел . Все положительные числа, состоящие из одинаковых цифр, переписать в массив . Удалить из массива числа с чётной суммой цифр.
  15. Заданы массивы целых чисел и . Все числа, с нечётной суммой цифр, переписать в массив . Найти три наибольших простых числа в массиве .
  16. Заданы массивы целых чисел и . Три наибольших числа из массива и числа из массива , в которых нет чётных цифр переписать в массив . Элементы массива вывести на экран в восьмеричной и десятичной системах счисления.
  17. Задан массив целых чисел . Семь наименьших простых чисел переписать в массив . Удалить из массива числа с чётной суммой цифр.
  18. Заданы массивы целых чисел и . Положительные числа из массива и пять наибольших чисел из массива переписать в массив . Найти сумму четырехзначных чисел массива .
  19. Заданы массивы целых положительных чисел: — в пятеричной, а в шестеричной системах счисления. Все числа из массивов переписать в массив десятичных чисел . В массиве найти пять наибольших чисел с нечётной суммой цифр.
  20. Заданы массивы целых положительных чисел . Все простые числа из массивов и , в которых есть цифры "1", "2" или "3" переписать в массив . Найти произведение двух наибольших и три наименьших простых чисел массива .
  21. Задан массив целых положительных чисел в двоичной системе . Из массива X сформировать массив десятеричных чисел . Из массива удалить четыре наименьших и три наибольших числа.
  22. Заданы массивы целых положительных чисел . Сформировать массив из элементов массивов , которые образуют убывающую последовательность цифр. Найти сумму семи наименьших чисел массива .
  23. Задан массив целых положительных чисел . Переписать в массив все числа-палиндромы, остальные числа переписать в массив . Удалить из массива все числа которые есть нули или сумма цифр нечётна.
  24. Заданы массивы целых положительных чисел . Числа, которые не состоят из одинаковых цифр, переписать в массив . Удалить из массива числа с чётной суммой цифр.
  25. Задан массив целых положительных чисел . Все числа с чётной суммой цифр переписать в массив . Элементы массива упорядочить в порядке убывания суммы цифр.

5.7.3 Работа с группами элементов в массиве

Разработать программу на языке C++ для решения следующей задачи.

  1. В массиве вещественных чисел найти предпоследнюю группу, которая состоит только из отрицательных элементов.
  2. В массиве вещественных чисел найти первую и последнюю группы знакочередующихся элементов.
  3. В массиве целых чисел найти вторую и третью группу, состоящую из нечётных цифр.
  4. В массиве целых чисел найти предпоследнюю группу, состоящую из возрастающей последовательности чисел.
  5. Из массива целых чисел удалить предпоследнюю группу, состоящую из возрастающей последовательности чисел.
  6. Из массива целых чисел удалить последнюю группу, состоящую из убывающей последовательности нечётных чисел.
  7. Из массива целых чисел удалить группу наибольшей длины, которая состоит из возрастающей последовательности нечётных чисел.
  8. В массиве целых чисел найти группу наименьшей длины, которая состоит из убывающей последовательности чётных чисел.
  9. Из массива целых чисел удалить две группы наибольшей длины, состоящие из простых чисел, в которых нет чётных цифр.
  10. Задан массив целых чисел. Вывести на экран первую и последнюю группы, состоящие из простых чисел.
  11. Из массива целых чисел удалить три группы наименьшей длины, состоящие из простых чисел, в представлении которых нет цифры семь.
  12. Из массива целых чисел удалить группу наибольшей длины, которая состоит из возрастающей последовательности простых чисел.
  13. Из массива целых чисел удалить все группы, которые состоят из убывающей последовательности чётных чисел.
  14. В массиве вещественных чисел найти группу максимальной длины, которая состоит из знакочередующихся чисел.
  15. В массиве вещественных чисел найти группу минимальной длины, которая состоит из убывающей последовательности чисел.
  16. Из массива вещественных чисел удалить все группы, состоящие из невозрастающей последовательности чисел.
  17. Из массива вещественных чисел удалить три группы наибольшей длины, состоящие из возрастающей последовательности чисел.
  18. В массиве целых чисел найти две последних группы, состоящие из простых чисел, причём цифры каждого числа образуют возрастающую последовательность.
  19. Из целочисленного массива удалить группу простых чисел минимальной длины, цифры которых образуют убывающей последовательность.
  20. Из целочисленного массива удалить группу минимальной длины, состоящую из элементов, представляющих собой возрастающую последовательность чётных цифр.
  21. В массиве целых чисел найти группы наименьшей и наибольшей длины, которые состоят из простых чисел.
  22. В массиве целых чисел найти группу наибольшей длины, которая состоит из неубывающей последовательности нечётных чисел.
  23. Из массива целых чисел удалить две группы наименьшей длины, состоящие из составных чисел, в записи которых нет цифр "0" и "2".
  24. Задан массив целых чисел. Вывести на экран первую и последнюю группы, состоящие из простых чисел с нечётной суммой цифр в каждом.
  25. Из массива целых чисел удалить три группы наибольшей длины, которые состоят из отрицательных чисел с чётной суммой цифр в каждом.

5.7.4 Сортировка элементов массива

Разработать программу на языке C++ для решения следующей задачи.

  1. Упорядочить по убыванию элементы целочисленного массива, расположенные между двумя наибольшими чётными значениями.
  2. Упорядочить в порядке возрастания модулей элементы массива, расположенные между наибольшим и наименьшим значениями.
  3. Упорядочить в порядке убывания модулей элементы, расположенные между первым и последним отрицательным значениями массива.
  4. Упорядочить в порядке убывания элементы, расположенные между вторым положительным и предпоследним отрицательным значениями массива.
  5. Упорядочить по возрастанию элементы целочисленного массива, расположенные между первым числом-палиндромом и последним отрицательным значением.
  6. Упорядочить в порядке возрастания суммы цифр элементы целочисленного массива, расположенные между последним числом-палиндромом и первым простым числом.
  7. Упорядочить по возрастанию модулей элементы целочисленного массива, расположенные между третьим и пятым простыми числами.
  8. Упорядочить по убыванию элементы целочисленного массива, расположенные после минимального числа-палиндрома.
  9. Удалить из целочисленного массива простые числа. В полученном массиве упорядочить по возрастанию модулей элементы, расположенные после наибольшего числа.
  10. Удалить из целочисленного массива числа-палиндромы. В полученном массиве упорядочить по возрастанию модулей элементы, расположенные до наименьшего простого числа.
  11. Удалить из целочисленного массива все составные числа. Упорядочить элементы массива в порядке возрастания суммы цифр чисел.
  12. Удалить из целочисленного массива все числа, состоящие из одинаковых цифр. Упорядочить элементы массива в порядке убывания суммы их цифр.
  13. Задан массив целых положительных чисел. Сформировать новый массив, куда записать элементы исходного массива, состоящие из одинаковых цифр. Упорядочить элементы полученного массива в порядке возрастания суммы цифр чисел.
  14. Упорядочить по возрастанию модулей элементы, расположенные между двумя наименьшими значениями массива.
  15. Упорядочить в порядке возрастания элементы, расположенные между четвёртым и девятым отрицательным числами массива.
  16. Упорядочить в порядке возрастания модулей элементы, расположенные между наибольшим и предпоследним положительным значениями массива.
  17. Упорядочить в порядке убывания модулей элементы, расположенные между пятым положительным и первым отрицательным значениями массива.
  18. Упорядочить в порядке убывания модулей элементы целочисленного массива, расположенные между наибольшим и наименьшим числамипалиндромами.
  19. Упорядочить в порядке убывания суммы цифр элементы целочисленного массива, расположенные между последним и предпоследним числамипалиндромами.
  20. Упорядочить по возрастанию модулей элементы массива, расположенные между двумя наименьшими положительными числами.
  21. Упорядочить по возрастанию элементы целочисленного массива, расположенные между двумя наибольшими числами-палиндромами.
  22. Удалить из целочисленного массива числа-палиндромы. В полученном массиве упорядочить по возрастанию модулей элементы, расположенные до наименьшего значения.
  23. Удалить из целочисленного массива отрицательные числа. В полученном массиве упорядочить по убыванию элементы, расположенные между двумя наибольшими простыми числами.
  24. Удалить из целочисленного массива простые числа. Упорядочить элементы массива в порядке убывания суммы цифр чисел.
  25. Задан массив целых положительных чисел. Сформировать новый массив, куда записать элементы исходного массива, состоящие из нечётных цифр. Упорядочить элементы полученного массива в порядке убывания суммы цифр чисел.

Лекция 6. Статические и динамические матрицы

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

6.1 Статические матрицы С(С++)

Матрица — это двумерный массив, каждый элемент которого имеет два индекса: номер строки — , номер столбца —.

Статический двумерный массив (матрицу) можно объявить так:

тип имя_переменной [n][m];

где тип определяет тип элементов массива, имя_переменной — имя матрицы, — количество строк, — количество столбцов в матрице. Строки нумеруются от 0 до , столбцы — от 0 до .

Например,

double x[20][35];

Описана матрица вещественных чисел , состоящая из 20 строк и 35 столбцов (строки нумеруются от 0 до 19, столбцы от 0 до 34).

Как и любой другой переменной, матрице можно присвоить начальное значение, например int A[2][3]={{1,2,3}, {4,5,6}};

Для обращения к элементу матрицы необходимо указать её имя, и в квадратных скобках номер строки, а затем в квадратных скобках — номер столбца. Например, x[2][4] — элемент матрицы x, находящийся в третьей строке и пятом столбце1).

Для работы с элементами матрицы необходимо использовать два цикла. Для построчной обработки матрицы значениями параметра первого (внешнего) цикла будут номера строк матрицы, значениями параметра второго (внутреннего) цикла — номера столбцов (см.рис. 6.1). При построчной обработке матрицы вначале поочерёдно рассматриваются элементы первой строки (столбца), затем второй и т.д. до последней. Если необходимо обрабатывать матрицу по столбцам, то необходимо организовать внешний цикл по столбцам, а внутренний по строкам (см. рис. 6.2).

6.2 Динамические матрицы

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

Блок-схема построчной обработки матрицы


Рис. 6.1.  Блок-схема построчной обработки матрицы

Блок-схема обработки матрицы по столбцам


Рис. 6.2.  Блок-схема обработки матрицы по столбцам

6.2.1 Использование указателей для работы с динамическими матрицами

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

int *A, N, M;
A=( int *) calloc (N*M, sizeof ( int ) );

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

A=( int *) malloc (N*M*sizeof ( int ) );

или операцию new

A=new int [N*M];

Память мы выделили, осталось найти способ обратиться к элементу матрицы. Все элементы матрицы хранятся в одномерном массиве размером элементов. Сначала в этом массиве расположена 0-я строка матрицы, затем 1-я и т.д. Поэтому для обращения к элементу A[i][j] необходимо по номеру строки и номеру столбца вычислить номер этого элемента в динамическом массиве. Учитывая, что в массиве элементы нумеруются с нуля, . Обращение к элементу A[i][j] будет таким: *(A+i*M+j) или A[i*M+j].

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

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

int main ( )
{
int N,M;
float **a;
a=new float *[N ];
}

С помощью оператора new создан массив из элементов2), в котором каждый элемент является адресом, где хранится указатель (фактически каждый указатель — адрес строки матрицы). Осталось определить значение элементов массива. Для этого организуем цикл (переменная цикла i изменяется от 0 до ), в котором будет выделяться память для хранения очередной строки матрицы. Адрес этой строки будет записываться в a[i].

for ( i =0; i<N; i++)
a [ i ]=new float [M];

После этого определён массив указателей, каждый из которых адресует массив из чисел (в данном случае вещественных типа float). Фактически создана динамическая матрица размера . Обращение к элементу динамической матрицы идёт так же, как и к элементу статической матрицы. Для того, чтобы обратиться к элементу в программе на C++, необходимо указать его имя, и в квадратных скобках номер строки и столбца (a[i][j]).

6.3 Обработка матриц в С(С++)

Рассмотрим основные операции, выполняемые над матрицами (статическими и динамическими) при решении задач.

Матрицы, как и массивы, нужно вводить (выводить) поэлементно. Блок- схема ввода элементов матрицы x[n][m] изображена на рис. 6.3.

Ввод элементов матрицы


Рис. 6.3.  Ввод элементов матрицы

Блок-схема построчного вывода матрицы


Рис. 6.4.  Блок-схема построчного вывода матрицы

При выводе матрицы элементы располагаются построчно, например:

Алгоритм построчного вывода элементов матрицы приведён на рис. 6.4.

Ниже приведён текст программы на C++ ввода-вывода статической матрицы.

#include <iostream>
using namespace std;
int main ( )
{
	int i, j,N,M, a [ 20 ] [ 20 ];
	cout<<" N = ";
	cin>>N; //Ввод количества строк
	cout<<" M = ";
	cin>>M; //Ввод количества столбцов
	cout<<"Ввод матрицы A "<<endl;
	for ( i =0; i<N; i++) //Цикл по переменной i, перебираем строки матрицы
		for ( j =0; j<M; j++) //Цикл по переменной j, перебираем элементы внутри строки
			cin>>a [ i ] [ j ]; //Ввод очередного элемента матрицы
	cout<<"Вывод матрицы A "<<endl;
	for ( i =0; i<N; i++) //Цикл по переменной i, перебираем строки матрицы
	{
		for ( j =0; j<M; j++) //Цикл по переменной j, перебираем строки матрицы
			cout<<a [ i ] [ j ]<<" \t "; //Вывод очередного элемента матрицы
		cout<<endl; //По окончанию вывода всех элементов строки — переход на новую строку.
	}
}

Цикл для построчного вывода матрицы можно записать и так.

for ( i =0; i<N; cout<<end l, i++)
	for ( j =0; j<M; j++)
		cout<<a [ i ] [ j ]<<" \t ";

При вводе матрицы элементы каждой строки можно разделять пробелами, символами табуляции или Enter. Ниже представлены результаты работы программы.

N=4
M=5
Ввод матрицы A
1 2 3
5 4
3 6 7 8 9
1 2 3 4 5 6 7 8 9 0
Вывод матрицы A
1	2	3	5	4
3	6	7	8	9
1	2	3	4	5
6	7	8	9	0

Далее на примерах решения практических задач будут рассмотрены основные алгоритмы обработки матриц и их реализация в C++. Перед этим давайте вспомним некоторые свойства матриц (рис. 6.5):

Свойства элементов матрицы


Рис. 6.5.  Свойства элементов матрицы

Задача 6.1. Найти сумму элементов матрицы, лежащих выше главной диагонали.

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

Блок-схема задачи 6.1 (алгоритм 1).


Рис. 6.6.  Блок-схема задачи 6.1 (алгоритм 1).

Блок-схема задачи 6.1 (алгоритм 2).


Рис. 6.7.  Блок-схема задачи 6.1 (алгоритм 2).

Текст программы:

#include <iostream>
using namespace std;
int main ( )
{
	int s, i, j, n,m, a [ 20 ] [ 20 ];
	cout<<" N = ";
	cin>>n;
	cout<<" M = ";
	cin>>m;
	cout<<"Ввод матрицы A "<<endl;
	for ( i =0; i<n; i++)
		for ( j =0; j<m; j++)
			cin>>a [ i ] [ j ];
	for ( s= i =0; i<n; i++)
		for ( j =0; j<m; j++)
			//Если элемент лежит выше главной диагонали, то наращиваем сумму.
			if ( j> i ) s+=a [ i ] [ j ];
	cout<<" S = "<<s<<endl;
}

На рис. 6.7 изображён ещё один алгоритм решения данной задачи. В нём проверка условия не выполняется, но, тем не менее, в нём так же суммируются элементы матрицы, находящиеся выше главной диагонали. В нулевой строке заданной матрицы необходимо сложить все элементы, начиная с первого. В первой — все, начиная со второго, в –й строке процесс начнётся с ()–го элемента и так далее. Таким образом, внешний цикл работает от 0 до , а второй от до . Авторы надеются, что читатель самостоятельно составит программу, соответствующую описанному алгоритму.

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

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

Из условия задачи понятно, что не нужно рассматривать все элементы заданной матрицы. Достаточно просмотреть первую и последнюю строки, первый и последний столбцы, а так же диагонали. Все эти элементы отмечены на схеме, причём чёрным цветом выделены элементы, обращение к которым может произойти дважды. Например, элемент с номером (0, 0) принадлежит как к нулевой строке, так и к нулевому столбцу, а элемент с номером () находится в последней строке и последнем столбце одновременно. Кроме того, если — число нечётное (на рис. 6.8 эта матрица расположена слева), то существует элемент с номером (), который находится на пересечении главной и побочной диагоналей. При нечётном значении (матрица справа на рис. 6.8) диагонали не пересекаются.

Рисунок к задаче 6.2.


увеличить изображение

Рис. 6.8.  Рисунок к задаче 6.2.

Итак, разобрав подробно постановку задачи, рассмотрим алгоритм её решения. Для обращения к элементам главной диагонали вспомним, что номера строк этих элементов всегда равны номерам столбцов. Поэтому, если параметр изменяется циклически от 0 до , то A[i][i] — элемент главной диагонали. Воспользовавшись свойством, характерным для элементов побочной диагонали, получим:

следовательно, для элемент А[i][N-i-1] — элемент побочной диагонали. Элементы, находящиеся по периметру матрицы записываются следующим образом: А[0][i] — нулевая строка, А[N-1][i] — последняя строка и соответственно А[i][0] — нулевой столбец, А[i][N-1] — последний столбец.

Текст программы решения задачи с пояснениями приведён далее.

#include <iostream>
using namespace std;
int main ( )
{
	int k, i, j,N, a [ 20 ] [ 20 ];
	cout<<" N = ";
	cin>>N;
	cout<<"Ввод матрицы A "<<endl;
	for ( i =0; i<N; i++)
		for ( j =0; j<N; j++)
			cin>>a [ i ] [ j ];
	//k — количество положительных элементов матрицы,
	//расположенных по её периметру и на диагоналях.
	for ( i=k=0; i<N; i++)
	{
		if ( a [ i ] [ i ] >0) k++; //Элемент лежит на главной диагонали.
		if ( a [ i ] [ N-i -1]>0)k++; //Элемент лежит на побочной диагонали.
	}
	for ( i =1; i<N-1; i++)
	{
		if ( a [ 0 ] [ i ] >0) k++; //Элемент находится в нулевой строке.
		if ( a [N-1 ] [ i ] >0) k++; //Элемент находится в последней строке.
		if ( a [ i ] [ 0 ] > 0 ) k++; //Элемент находится в нулевом столбце.
		if ( a [ i ] [ N-1]>0)k++; //Элемент находится в последнем столбце.
	}
	//Элемент, находящийся на пересечении диагоналей, подсчитан дважды,
	//надо уменьшить вычисленное значение k на один.
	if ( (N%2!=0)&&(a [N/ 2 ] [N/2 ] >0) ) k--;
	cout<<" k = "<<k<<endl;
}

Задача 6.3. Проверить, является ли заданная квадратная матрица единичной.

Единичной называют матрицу, у которой элементы главной диагонали — единицы, а все остальные — нули. Например,

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

#include <iostream>
using namespace std;
int main ( )
{
	int pr, i, j,N, **a;
	cout<<" N = "; //Ввод размерности матрицы
	cin>>N;
	a=new int * [N ]; //Создаём квадратную динамическую матрицу
	for ( i =0; i<N; i++)
		a [ i ]=new int [N ];
	cout<<"Ввод элементов матрицы A "<<endl;
	for ( i =0; i<N; i++)
		for ( j =0; j<N; j++)
			cin>>a [ i ] [ j ];
	//Предположим, что матрица единичная, и присвоим переменной pr значение 1 (истина).
	//Если значение этой переменной при выходе из цикла не изменится, это будет означать,
	//что матрица действительно единичная.
	for ( pr =1, i =0; i<N; i++)
		for ( j =0; j<N; j++)
		if ( ( ( i==j )&&(a [ i ] [ j ] ! = 1 ) ) | | ( ( i != j )&&(a [ i ] [ j ] ! = 0 ) ) )
		//Если элемент лежит на главной диагонали и не равен единице или элемент лежит вне
		//главной диагонали и не равен нулю, то
		{
			pr =0; //Переменной pr присвоить значение 0 (ложь), это будет означать,
			break; //что матрица единичной не является, и выйти из цикла.
		}
	//Проверка значения переменной pr и вывод соответствующего сообщения.
	if ( pr ) cout<<"Единичная матрица\n ";
	else cout<<"Матрица не является единичной\n ";
}

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

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

#include <iostream>
using namespace std;
int main ( )
{
	int i, j,N,M;
	double S, **a;
	cout<<" N = "; //Ввод размерности матрицы.
	cin>>N;
	cout<<" M = ";
	cin>>M;
	a=new double * [N ]; //Создаём динамическую матрицу
	for ( i =0; i<N; i++)
		a [ i ]=new double [M];
	cout<<"Ввод элементов матрицы A "<<endl;
	for ( i =0; i<N; i++)
		for ( j =0; j<M; j++)
			cin>>a [ i ] [ j ];
	//Цикл по i завершается записью среднего значения в нулевой элемент строки и наращиванием i.
	for ( i =0; i<N; a [ i ] [ 0 ] = S/M, i++)
		for ( S=j =0; j<M; j++) //Вычисление суммы элементов строки.
			S+=a [ i ] [ j ];
	cout<<"Преобразованная матрица A "<<endl;
	for ( i =0; i<N; cout<<end l, i++)
		for ( j =0; j<M; j++)
			cout<<a [ i ] [ j ]<<" \t ";
}

Задача 6.5. Задана матрица A(n,m). Поменять местами её максимальный и минимальный элементы.

Алгоритм решения этой задачи следующий: находим максимальный элемент матрицы (max) и его индексы (imax, jmax), а также минимальный (min) и его индексы (imin, jmin). После чего элементы A[imax][jmax] и A[imin][jmin] поменяем местами. Для поиска максимального элемента и его индексов в переменную max запишем A[0][0], в переменные imax, jmax (номер строки и столбца, где находятся максимальный элемент) запишем 0. Затем в двойном цикле (цикл по переменной — по строкам, цикл по переменной — по столбцам) перебираем все элементы, и каждый из них сравниваем с максимальным (со значением переменной max). Если текущий элемент массива оказывается больше максимального, то его переписываем в переменную max, а в переменную imax — текущее значение индекса , в переменную jmax — текущее значение . Поиск минимального элемента матрицы аналогичен и отличается только знаком операции сравнения. Далее представлен текст программы решения задачи 6.5.

#include <iostream>
using namespace std;
int main ( )
{
	int i, j, imax, jmax, imin, jmin,N,M;
	double min, max, b, **a;
	cout<<" N = "; //Ввод размеров матрицы.
	cin>>N;
	cout<<" M = ";
	cin>>M;
	a=new double * [N ]; //Создаём динамическую матрицу
	for ( i =0; i<N; i++)
		a [ i ]=new double [M];
	cout<<"Ввод элементов матрицы A "<<endl;
	for ( i =0; i<N; i++)
		for ( j =0; j<M; j++)
			cin>>a [ i ] [ j ];
	//Двойной цикл для поиска максимального, минимального элементов и их индексов.
	for (max=min=a [ 0 ] [ 0 ], imax=jmax=imin=jmin= i =0; i<N; i++)
		for ( j =0; j<M; j++)
		{
			if ( a [ i ] [ j ]>max) {max=a [ i ] [ j ]; imax= i; jmax=j; }
			if ( a [ i ] [ j ]<min ) {min=a [ i ] [ j ]; imin= i; jmin=j; }
		}
	//Обмен двух элементов матрицы.
	b=a [ imax ] [ jmax ];
	a [ imax ] [ jmax ]=a [ imin ] [ jmin ];
	a [ imin ] [ jmin ]=b;
	//Вывод преобразованной матрицы.
	cout<<"Преобразованная матрица A "<<endl;
	for ( i =0; i<N; cout<<end l, i++)
		for ( j =0; j<M; j++)
			cout<<a [ i ] [ j ]<<" \t ";
}

Задача 6.6. Преобразовать матрицу A(m,n) так, чтобы строки с нечётными индексами были упорядочены по убыванию, с чётными — по возрастанию.

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

#include <iostream>
using namespace std;
int main ( )
{
	int i, j, k,N,M;
	double b, **a;
	cout<<" M = "; //Ввод размеров матрицы.
	cin>>M;
	cout<<" N = ";
	cin>>N;
	a=new double * [N ]; //Создаём динамическую матрицу
	for ( i =0; i<N; i++)
		a [ i ]=new double [M];
	cout<<"Ввод элементов матрицы A "<<endl;
	for ( i =0; i<M; i++)
		for ( j =0; j<N; j++)
			cin>>a [ i ] [ j ];
	for ( i =0; i<M; i++) //Цикл по i — для перебора строк матрицы.
		if ( i%2==0) //Если строка чётна, то
		{ //упорядочиваем элементы строки по возрастанию,
		for ( k=1;k<N; k++)
			for ( j =0; j<N-k; j++)
			if ( a [ i ] [ j ]>a [ i ] [ j +1 ])
			{
				b=a [ i ] [ j ];
				a [ i ] [ j ]=a [ i ] [ j + 1 ];
				a [ i ] [ j +1]=b;
			}
		}
		else //иначе нечётные строки упорядочиваем по убыванию.
			for ( k=1;k<N; k++)
				for ( j =0; j<N-k; j++)
					if ( a [ i ] [ j ]<a [ i ] [ j +1 ])
					{
						b=a [ i ] [ j ];
						a [ i ] [ j ]=a [ i ] [ j + 1 ];
						a [ i ] [ j +1]=b;
					}
	cout<<"Преобразованная матрица A "<<endl; //Вывод преобразованной матрицы.
	for ( i =0; i<M; cout<<endl, i++)
	for ( j =0; j<N; j++)
	cout<<a [ i ] [ j ]<<" \t ";
}

Блок-схема алгоритма задачи 6.6.


увеличить изображение

Рис. 6.9.  Блок-схема алгоритма задачи 6.6.

Задача 6.7. Поменять местами элементы главной и побочной диагонали матрицы .

Алгоритм решения задачи следующий: перебираем все строки матрицы (цикл по переменной от 0 до в тексте программы), и в каждой строке меняем местами элементы, расположенные на главной и побочной диагоналях (в -й строке надо поменять местами элементы A[i][i] и А[i][k-i-1]). Текст программы с комментариями приведён далее.

#include <iostream>
using namespace std;
int main ( )
{
	int i, j, k;
	double b, ** a;
	cout<<" k = "; //Ввод размера матрицы.
	cin>>k;
	a=new double * [ k ]; //Создаём динамическую матрицу
	for ( i =0; i<k; i++)
		a [ i ]=new double [ k ];
	cout<<"Ввод элементов матрицы A "<<endl;
	for ( i =0; i<k; i++)
		for ( j =0; j<k; j++)
			cin>>a [ i ] [ j ];
	for ( i =0; i<k; i++) //Цикл по строкам.
	{//В каждой строке обмен между элементами, лежащими на главной и побочной диагоналях.
		b=a [ i ] [ i ];
		a [ i ] [ i ]=a [ i ] [ k-1-i ];
		a [ i ] [ k-1-i ]=b;
	}
	cout<<"Преобразованная матрица A "<<endl; //Вывод преобразованной матрицы.
	for ( i =0; i<k; cout<<end l, i++)
		for ( j =0; j<k; j++)
			cout<<a [ i ] [ j ]<<" \t ";
}

Задача 6.8. Заполнить матрицу числами от 1 до 36 следующим образом:

Последовательно построчно заполняем матрицу возрастающей арифметической последовательностью 1, 2, 3,..., 36. Чётные строки заполняем от нулевого элемента к последнему, а нечётные — от последнего к нулевому. Текст программы приведён далее.

#include <iostream>
using namespace std;
int main ( int arg c, char **argv )
{
	int **a, n=6,k=0, i, j;
	a=new int * [ n ]; //Выделяем память для хранения матрицы
	for ( i =0; i<n; i++)
		a [ i ]=new int [ n ];
	for ( i =0; i<n; i++) //Перебираем все строки матрицы.
		if ( i%2==0) //Строки с чётными номерами заполняем возрастающей последовательностью
			for ( j =0; j<n; j++) //чисел слева направо
				a [ i ] [ j ]=++k;
		else //Строки с нечётными номерами заполняем возрастающей последовательностью чисел
			for ( j=n-1; j >=0; j --) //справа налево
				a [ i ] [ j ]=++k;
		cout<<"Вывод матрицы A "<<endl;
		for ( i =0; i<n; cout<<end l, i++)
			for ( j =0; j<n; j++)
				cout<<a [ i ] [ j ]<<" \t ";
	return 0;
}

6.4 Решение некоторых задач линейной алгебры

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

Задача 6.9.Заданы четыре матрицы вещественных чисел. Вычислить матрицу .

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

Напомним алгоритм умножения матриц на примере

Воспользовавшись правилом "строка на столбец", получим матрицу:

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

где и .

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

При решении задачи будем использовать динамические матрицы и двойные указатели. Напишем следующие функции.

Далее приведён текст программы с комментариями.

#include <iostream>
using namespace std;
//функция вычисления суммы двух матриц.
float **sum_m( float **A, float **B, int N, int M)
{
	int i, j;
	float **temp; //указатель для хранения результирующей матрицы
	temp=new float * [N ]; //выделение памяти для хранения результирующей матрицы
	for ( i =0; i<N; i++)
		temp [ i ]=new float [M];
	for ( i =0; i<N; i++) //Вычисляем сумму двух матриц
		for ( j =0; j<M; j++)
			temp [ i ] [ j ]=A [ i ] [ j ]+B [ i ] [ j ];
	return temp; //Возвращаем матрицу как двойной указатель
}
//функция вычисления разности двух матриц.
float **minus_m ( float **A, float **B, int N, int M)
{ int i, j;
	float **temp; //указатель для хранения результирующей матрицы
	temp=new float * [N ]; //выделение памяти для хранения результирующей матрицы
	for ( i =0; i<N; i++)
		temp [ i ]=new float [M];
	for ( i =0; i<N; i++) //Вычисляем разность двух матриц
		for ( j =0; j<M; j++)
			temp [ i ] [ j ]=A [ i ] [ j ]-B [ i ] [ j ];
	return temp; //Возвращаем матрицу как двойной указатель
}
//функция вычисления произведения двух матриц.
float **product_m ( float **A, float **B, int N, int M, int L)
{
	int i, j, k;
	float **temp; //указатель для хранения результирующей матрицы
	temp=new float * [N ]; //выделение памяти для хранения результирующей матрицы
	for ( i =0; i<N; i++)
		temp [ i ]=new float [ L ];
	//Вычисляем произведение двух матриц, последовательно формируя все элементы матрицы
	for ( i =0; i<N; i++)
		for ( j =0; j<L; j++)
			//Элемент с индексами i, j — скалярное произведение i-й строки матрицы A
			for ( temp [ i ] [ j ]=k=0;k<M; k++) //и j-го столбца матрицы B
				temp [ i ] [ j ]+=A [ i ] [ k ] *B [ k ] [ j ];
	return temp; //Возвращаем матрицу как двойной указатель
}
//функция создаёт динамическую матрицу вещественных чисел размерности N на M,
//в этой же функции осуществляется и ввод элементов матрицы
float ** create_m ( int N, int M)
{
	int i, j;
	float **temp;
	temp=new float * [N ];
	for ( i =0; i<N; i++)
		temp [ i ]=new float [M];
	cout<<"Ввод матрицы\n ";
	for ( i =0; i<N; i++)
		for ( j =0; j<M; j++)
			cin>>temp [ i ] [ j ];
	return temp;
}
//функция осуществляет построчный вывод матрицы A(N,M)
void output_m ( float **A, int N, int M)
{
	int i, j;
	//Цикл по строкам. По окончанию вывода всех элементов строки — переход на новую строку.
	for ( i =0; i<N; cout<<endl, i++)
		for ( j =0; j<M; j++) //Цикл по переменной j, в котором перебираем строки матрицы
			cout<<A [ i ] [ j ]<<" \t "; //Вывод очередного элемента матрицы и символа табуляции.
}
int main ( int arg c, char ** argv )
{
	float **A, **B, **C, **D, ** result; //указатели для хранения исходных и
	результирующей матриц
	int N,M;
	cout<<" N = "; cin>>N; //Ввод размерностей матрицы
	cout<<" M = "; cin>>M;
	//Выделение памяти и ввод матриц A, B, C, D, обращением к функции create_m.
	A=create_m (N,M);
	B=create_m (N,M);
	C=create_m (M,N);
	D=create_m (M,N);
	//Вычисление результирующей матрицы.
	result=product_m ( product_m (sum_m(A, B,N,M),minus_m (C,D,M,N),N,M,N), product_m
		(sum_m(A, B,N,M),minus_m (C,D,M,N),N,M,N),N,N,N);
	output_m ( result,N,N); //Вывод результирующей матрицы.
	return 0;
}

Далее без комментариев приведена программа решения задачи 6.9 с помощью динамических матриц и обычных указателей1). Рекомендуем читателям самостоятельно разобраться с этой версией программы.

#include <iostream>
using namespace std;
float *sum_m( float *A, float *B, int N, int M)
{
	int i, j;
	float *temp;
	temp=new float [N*M];
	for ( i =0; i<N; i++)
		for ( j =0; j<M; j++)
			temp [ i *M+j ]=A [ i *M+j ]+B [ i *M+j ];
	return temp;
}
float *minus_m ( float *A, float *B, int N, int M)
{ int i, j;
	float *temp;
	temp=new float [N*M];
	for ( i =0; i<N; i++)
		for ( j =0; j<M; j++)
			temp [ i *M+j ]=A [ i *M+j ]-B [ i *M+j ];
	return temp;
}
float *product_m ( float *A, float *B, int N, int M, int L)
{
	int i, j, k;
	float *temp;
	temp=new float [N*L ];
	for ( i =0; i<N; i++)
		for ( j =0; j<L; j++)
			for ( temp [ i *L+j ]=k=0;k<M; k++)
				temp [ i *L+j ]+=A [ i *M+k ] *B [ k*L+j ];
	return temp;
}
float *create_m ( int N, int M)
{
	int i, j;
	float *temp;
	temp=new float [N*M];
	cout<<"Ввод матрицы\n ";
	for ( i =0; i<N; i++)
		for ( j =0; j<M; j++)
			cin>>temp [ i *M+j ];
	return temp;
}
void output_m ( float *A, int N, int M)
{
	int i, j;
	for ( i =0; i<N; cout<<endl, i++)
		for ( j =0; j<M; j++)
			cout<<A [ i *M+j ]<<" \t ";
}
int main ( int arg c, char ** argv )
{
	float *A, *B, *C, *D, * result;
	int N,M;
	cout<<" N = "; cin>>N;
	cout<<" M = "; cin>>M;
	A=create_m (N,M);
	B=create_m (N,M);
	C=create_m (M,N);
	D=create_m (M,N);
	result=product_m ( product_m (sum_m(A, B,N,M), minus_m (C,D,M,N),N,M,N),
		product_m (sum_m(A, B,N,M), minus_m (C,D,M,N),N,M,N),N,N,N);
	output_m ( result,N,N);
	return 0;
}

Задача 6.10. Решить систему линейных алгебраических уравнений.

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

Пусть дана система линейных алгебраических уравнений (СЛАУ) с неизвестными

(6.1)

Обозначим через

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

Наиболее распространённым приёмом решения систем линейных уравнений является алгоритм последовательного исключения неизвестных — метод Гаусса.

При решении систем линейных алгебраических уравнений этим методом всевозможные преобразования производят не над уравнениями системы (6.1), а над так называемой расширенной матрицей системы, которая получается путём добавления к основной матрице столбца свободных членов .

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

(6.2)

На первом этапе необходимо обнулить элементы 0-го столбца расширенной матрицы

(6.3)

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

1-я строка = 1-я строка – -я строка

2-я строка = 2-я строка – -я строка

...

-я строка =-я строка – -я строка

...

-я строка = -я строка – M\times 0-я строка

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

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

Элементы второй строки и коэффициент можно рассчитать аналогично:

Таким образом, преобразование элементов –й строки будет происходить следующим образом:

Коэффициент для –й строки выбирается из условия и равен .

После проведения подобных преобразований для всех строк матрица (6.2) примет вид

Блок-схема обнуления первого столбца матрицы приведена на рис. 6.10.

Очевидно, что если повторить описанный выше алгоритм для следующих столбцов матрицы (6.2), то в результате будет получена матрица (6.3). Алгоритм этого процесса изображён на рис. 6.11.

Блок-схема обнуления первого столбца матрицы


Рис. 6.10.  Блок-схема обнуления первого столбца матрицы

Блок-схема алгоритма преобразования расширенной матрицы к треугольному виду


Рис. 6.11.  Блок-схема алгоритма преобразования расширенной матрицы к треугольному виду

Заметим, что если в матрице (6.2) на главной диагонали встретится элемент , равный нулю, то расчёт коэффициента для kй строки будет невозможен. Избежать деления на ноль можно, избавившись от нулевых элементов на главной диагонали. Для этого перед обнулением элементов в –м столбце необходимо найти в нём максимальный по модулю элемент (среди расположенных ниже ), запомнить номер строки, в которой он находится, и поменять её местами с -й. Алгоритм, отображающий эти преобразования, приведён на рис. 6.12.

В результате выполнения прямого хода метода Гаусса матрица (6.2) преобразуется в матрицу (6.3), а система уравнений (6.1) будет иметь следующий вид:

(6.4)

Решение системы (6.4) называют обратным ходом метода Гаусса.

Последнее -е уравнение системы (6.4) имеет вид: . Тогда, если , то . В случае, если a, и , то система (6.4), а следовательно, и система (6.1) имеют бесконечное множество решений.

При и система (6.4), а значит и система (6.1), решения не имеет. Предпоследнее -е уравнение системы (6.4) имеет вид .

Блок-схема алгоритма перестановки строк расширенной матрицы


Рис. 6.12.  Блок-схема алгоритма перестановки строк расширенной матрицы

Блок-схема алгоритма обратного хода метода Гаусса


Рис. 6.13.  Блок-схема алгоритма обратного хода метода Гаусса

Значит, .

Следующее -е уравнение системы (6.4) будет выглядеть так:

Отсюда имеем

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

Алгоритм, реализующий обратный ход метода Гаусса, представлен в виде блок-схемы на рис. 6.13.

Объединив блок-схемы, изображённые на рис. 6.11,рис. 6.12 и рис. 6.13, получим общую блок-схему метода Гаусса (рис. 6.14). Блоки 2-6 содержат последовательный ввод данных, где — это размерность системы линейных алгебраических уравнений, а сама система задаётся в виде матрицы коэффициентов при неизвестных и вектора свободных коэффициентов . Блоки 7-18 предусматривают прямой ход метода Гаусса, а блоки 23-27 — обратный. Для вывода результатов предусмотрено несколько блоков вывода. Если результат проверки условий 19 и 20 положительный, то выдаётся сообщение о том, что система имеет бесконечное множество решений (блок 21). Если условие 19 выполняется, а 20 — нет, то появляется сообщение о том, что система не имеет решений (блок 22). Сами же решения системы уравнений, представленные вектором , вычисляются (блоки 23–26) и выводятся экран/печать (блок 27) только в случае невыполнения условия.

Теперь алгоритм решения СЛАУ, представленный на рис. 6.14, разобьём на главную функцию main() и функцию решения СЛАУ методом Гаусса. В функции main() будет находиться ввод исходных данных, обращение к функции SLAU и вывод вектора решения. Функция SLAU предназначена для решения системы линейных алгебраических уравнений методом Гаусса.

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

Функция SLAU возвращает значение 0, если решение найдено, -1 — если система имеет бесконечное множество решений, -2 — если система не имеет решений.

Ниже приведено решение задачи 6.10 с подробными комментариями.

#include <iostream>
#include <math.h>
using namespace std;
int SLAU( double ** matrica_a, int n, double *massiv_b, double *x )
//Функция SLAU возвращает значение типа int: 0, если решение найдено, _1 — если система имеет
//бесконечное множество решений, _2 — если система не имеет решений.
//Формальные параметры функции: n — размерность системы,
//matrica_a — матрица коэффициентов СЛАУ,
//massiv_b — вектор правых частей, x — решение СЛАУ, передаются как указатели.
{
	int i, j, k, r;
	double c,M, max, s;
	//Матрица a — копия матрицы коэффициентов, массив b — копия вектора правых частей.
	double **a, *b;
	a=new double * [ n ]; //Выделение памяти для a и b.
	for ( i =0; i<n; i++)
		a [ i ]=new double [ n ];
	b=new double [ n ];
	//В a записываем копию матрицы коэффициентов, в b копию вектора правых частей.
	for ( i =0; i<n; i++)
		for ( j =0; j<n; j++)
			a [ i ] [ j ]=matrica_a [ i ] [ j ];
			for ( i =0; i<n; i++)
				b [ i ]=massiv_b [ i ];
	//Прямой ход метода Гаусса: приводим матрицу a (копию матрицы коэффициентов СЛАУ)
	//к диагональному виду.
	for ( k=0;k<n; k++)
	{ //Поиск максимального по модулю элемента в k-м столбце.
		max=fabs ( a [ k ] [ k ] );
		r=k;
		for ( i=k+1; i<n; i++)
			if ( fabs ( a [ i ] [ k ] )>max)
			{
				max=fabs ( a [ i ] [ k ] );
				r= i;
			}
		for ( j =0; j<n; j++) //Меняем местами k-ю и r-ю (строку, где находится
		{ //максимальный по модулю элемент) строки.
			c=a [ k ] [ j ];
			a [ k ] [ j ]=a [ r ] [ j ];
			a [ r ] [ j ]= c;
		}
		c=b [ k ];
		b [ k ]=b [ r ];
		b [ r ]= c;
		for ( i=k+1; i<n; i++) //Приведение матрицы к диагональному виду.
		{
			for (M=a [ i ] [ k ] / a [ k ] [ k ], j=k; j<n; j++)
				a [ i ] [ j ]-=M*a [ k ] [ j ];
				b [ i ]-=M*b [ k ];
		}
	}
	//Обратный ход метода Гаусса.
	if ( a [ n-1 ] [ n-1]==0) //Если последний диагональный элемент равен 0 и
		if ( b [ n-1]==0) //последний коэффициент вектора свободных членов равен 0,
			return -1; //то система имеет бесконечное множество решений
		else return -2; //последний коэффициент вектора свободных членов не равен 0,
		//система решений не имеет.
	else //Последний диагональный элемент не равен 0, начинается обратный ход метода Гаусса.
	{
		for ( i=n-1; i >=0; i --)
		{
			for ( s =0, j= i +1; j<n; j++)
				s+=a [ i ] [ j ] * x [ j ];
			x [ i ]=( b [ i ]- s ) / a [ i ] [ i ];
		}
		return 0;
	}
}
int main ( )
{
	int result, i, j,N;
	double **a, *b, *x;
	cout<<" N = "; //Ввод размерности системы.
	cin>>N;
	a=new double * [N ]; //Выделение памяти для матрицы правых частей и вектора свободных
	членов.
	for ( i =0; i<N; i++)
		a [ i ]=new double [N ];
	b=new double [N ];
	x=new double [N ];
	cout<<"Ввод матрицы A "<<endl; //Ввод матрицы правых частей
	for ( i =0; i<N; i++)
		for ( j =0; j<N; j++)
			cin>>a [ i ] [ j ];
	cout<<"Ввод вектора B "<<endl; //и вектора свободных членов.
	for ( i =0; i<N; i++)
		cin>>b [ i ];
	//Вызов функции решения СЛАУ методом Гаусса. По значению result можно судить, сколько
	//корней имеет система. Если result=0, то система имеет единственное решение, result= -1 -
	//система имеет бесконечное множество решений, result=-2 — система не имеет решений.
	result=SLAU( a,N, b, x );
	if ( result ==0)
	{ //Вывод массива решения.
		cout<<" MassivX "<<endl;
		for ( i =0; i<N; i++)
			cout<<x [ i ]<<" \t ";
		cout<<endl;
	}
	else if ( result ==-1)
		cout<<"Бесконечное множество решений\n ";
			else if ( result ==-2)
				cout<<"Нет решений\n ";
}

Блок-схема алгоритма решения СЛАУ методом Гаусса


Рис. 6.14.  Блок-схема алгоритма решения СЛАУ методом Гаусса

Задача 6.11. Найти обратную матрицу к квадратной матрице .

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

(6.5)

Необходимо найти матрицу , которая является обратной к матрице :

(6.6)

Матрица (6.6) будет обратной к матрице (6.5), если выполняется соотношение , где — это единичная матрица, или более подробно:

(6.7)

Результат перемножения матриц из соотношения (6.7) можно представить поэлементно в виде систем линейных уравнений. Умножение матрицы (6.5) на нулевой столбец матрицы (6.6) даст нулевой столбец единичной матрицы:

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

Система, полученная в результате умножения матрицы (6.5) на -й столбец матрицы (6.6), будет выглядеть следующим образом:

Понятно, что -я система будет иметь вид:

Решением каждой из приведённых выше систем будет -й столбец обратной матрицы. Количество систем равно размерности обратной матрицы. Для отыскания решений систем линейных алгебраических уравнений можно воспользоваться методом Гаусса.

Описанный алгоритм представлен в виде блок-схемы на рис. 6.15. Блоки 2–5 отражают формирование вектора правых частей системы линейных алгебраических уравнений. Если условие в блоке 3 выполняется и элемент находится на главной диагонали, то он равен единице, все остальные элементы нулевые. В блоке 6 происходит вызов подпрограммы для решения системы уравнений методом Гаусса. В качестве параметров в эту подпрограмму передаётся исходная матрица , сформированный в блоках 2–5 вектор свободных коэффициентов , размерность системы . Вектор будет решением -й системы уравнений и, следовательно, -м столбцом искомой матрицы .

Как видно из блок-схемы, приведённой на рис. 6.15, при нахождении обратной матрицы понадобится функция SLAU, рассмотренная при решении задачи 6.10. Ниже приведён текст программы с подробными комментариями решения задачи 6.11. В функции main() будет находиться ввод исходной матрицы, обращение к функции INVERSE для вычисления обратной матрицы. Из функции INVERSE будет осуществляться вызов функции SLAU для решения системы линейных алгебраических уравнений.

#include <iostream>
#include <math.h>
using namespace std;
//Функция решения системы линейных алгебраических уравнений методом Гаусса.
int SLAU( double ** matrica_a, int n, double *massiv_b, double *x )
{
	int i, j, k, r;
	double c,M, max, s;
	double **a, *b;
	a=new double * [ n ];
	for ( i =0; i<n; i++)
		a [ i ]=new double [ n ];
	b=new double [ n ];
	for ( i =0; i<n; i++)
		for ( j =0; j<n; j++)
			a [ i ] [ j ]=matrica_a [ i ] [ j ];
	for ( i =0; i<n; i++)
		b [ i ]=massiv_b [ i ];
	for ( k=0;k<n; k++)
		{
		max=fabs ( a [ k ] [ k ] );
		r=k;
		for ( i=k+1; i<n; i++)
			if ( fabs ( a [ i ] [ k ] )>max)
			{
				max=fabs ( a [ i ] [ k ] );
				r= i;
			}
		for ( j =0; j<n; j++)
		{
			c=a [ k ] [ j ];
			a [ k ] [ j ]=a [ r ] [ j ];
			a [ r ] [ j ]= c;
		}
		c=b [ k ];
		b [ k ]=b [ r ];
		b [ r ]= c;
		for ( i=k+1; i<n; i++)
		{
			for (M=a [ i ] [ k ] / a [ k ] [ k ], j=k; j<n; j++)
				a [ i ] [ j ]*=M_a [ k ] [ j ];
			b [ i ]_=M_b [ k ];
		}
	}
	if ( a [ n -1 ] [ n-1]==0)
		if ( b [ n-1]==0)
			return -1;
		else return -2;
	else
	{
		for ( i=n-1; i >=0; i --)
		{
			for ( s =0, j= i +1; j<n; j++)
				s+=a [ i ] [ j ] * x [ j ];
			x [ i ]=( b [ i ]- s ) / a [ i ] [ i ];
		}
		return 0;
	}
	for ( i =0; i<n; i++)
	delete [ ] a [ i ];
	delete [ ] a;
	delete [ ] b;
}
//Функция вычисления обратной матрицы
int INVERSE( double **a, int n, double **y )
//Формальные параметры: a — исходная матрица, n — размерность матрицы, y — обратная
матрица.
//Функция будет возвращать 0, если обратная матрица существует, -1 — в противном случае.
{
	int i, j, res;
	double *b, *x;
	//Выделение памяти для промежуточных массивов b и x.
	b=new double [ n ];
	x=new double [ n ];
	for ( i =0; i<n; i++)
	{
		//Формирование вектора правых частей для нахождения i-го столбца матрицы.
		for ( j =0; j<n; j++)
			if ( j==i )
				b [ j ]= 1;
		else b [ j ]= 0;
		//Нахождение i-го столбца матрицы путём решения СЛАУ Ax = b методом Гаусса.
		res=SLAU( a, n, b, x );
		//Если решение СЛАУ не найдено, то невозможно вычислить обратную матрицу.
		if ( res !=0)
			break;
		else
		//Формирование i-го столбца обратной матрицы.
		for ( j =0; j<n; j++)
			y [ j ] [ i ]=x [ j ];
	}
	//Проверка существования обратной матрицы, если решение одного из уравнений Ax=b не
	//существует, то невозможно найти обратную матрицу, и функция INVERSE вернёт значение -1.
	if ( res !=0)
		return -1;
	//Если обратная матрица найдена, то функция INVERSE вернёт значение 0,
	//а обратная матрица будет возвращаться через указатель double **y.
	else
		return 0;
}
int main ( )
{
int result, i, j,N;
double **a, **b; //Двойные указатели для хранения исходной a и обратной b матрицы.
cout<<" N = "; //Ввод размера матрицы.
cin>>N;
a=new double * [N ]; //Выделение памяти для матриц a и b.
for ( i =0; i<N; i++)
	a [ i ]=new double [N ];
b=new double * [N ];
for ( i =0; i<N; i++)
b [ i ]=new double [N ];
cout<<"Ввод матрицы A "<<endl; //Ввод исходной матрицы.
for ( i =0; i<N; i++)
	for ( j =0; j<N; j++)
		cin>>a [ i ] [ j ];
result=INVERSE( a,N, b ); //Вычисление обратной матрицы.
if ( result ==0) //Если обратная матрица существует, то вывести её на экран.
{
	cout<<"Обратная матрица"<<endl;
	for ( i =0; i<N; cout<<endl, i++)
	for ( j =0; j<N; j++)
		cout<<b [ i ] [ j ]<<" \t ";
}
else
	//Если обратная матрица не существует, то вывести соответствующее сообщение.
	cout<<"Нет обратной матрицы"<<endl;
}

Задача 6.12. Найти определитель квадратной матрицы .

Блок-схема алгоритма вычисления обратной матрицы


Рис. 6.15.  Блок-схема алгоритма вычисления обратной матрицы

Пусть задана матрица (6.2), необходимо вычислить её определитель. Для этого матрицу необходимо преобразовать к треугольному виду (6.3), а затем воспользоваться свойством, известным из курса линейной алгебры, которое гласит, что определитель треугольной матрицы равен произведению её диагональных элементов: .

Преобразование матрицы (6.2) к виду (6.3) можно осуществить с помощью прямого хода метода Гаусса. Алгоритм вычисления определителя матрицы, изображённый в виде блок-схемы на рис. 6.16, представляет собой алгоритм прямого хода метода Гаусса, в процессе выполнения которого проводится перестановка строк матрицы. Эта операция приводит к смене знака определителя. В блок- схеме момент смены знака отражён в блоках 8–9. В блоке 8 определяется, будут ли строки меняться местами, и если ответ утвердительный, то в блоке 9 происходит смена знака определителя. В блоках 15–16 выполняется непосредственное вычисление определителя путём перемножения диагональных элементов преобразованной матрицы.

На листинге приведён текст программы решения задачи 6.12 с комментариями.

#include <iostream>
#include <math.h>
using namespace std;
//Функция вычисления определителя.
double determinant ( double ** matrica_a, int n )
//Формальные параметры: matrica_a — исходная матрица, n — размер матрицы,
//функция возвращает значение определителя (тип double.)
{
	int i, j, k, r;
	double c,M, max, s, det =1;
	//a — копия исходной матрицы.
	double **a;
	//Выделение памяти для матрицы a .
	a=new double * [ n ];
	for ( i =0; i<n; i++)
		a [ i ]=new double [ n ];
	//В a записываем копию исходной матрицы.
	for ( i =0; i<n; i++)
		for ( j =0; j<n; j++)
			a [ i ] [ j ]=matrica_a [ i ] [ j ];
	//Прямой ход метода Гаусса.
	for ( k=0;k<n; k++)
	{
		max=fabs ( a [ k ] [ k ] );
		r=k;
		for ( i=k+1; i<n; i++)
			if ( fabs ( a [ i ] [ k ] )>max)
			{
				max=fabs ( a [ i ] [ k ] );
				r= i;
			}
		//Если строки менялись местами, то смена знака определителя.
		if ( r !=k ) det=-det;
		for ( j =0; j<n; j++)
		{
			c=a [ k ] [ j ];
			a [ k ] [ j ]=a [ r ] [ j ];
			a [ r ] [ j ]= c;
		}
		for ( i=k+1; i<n; i++)
			for (M=a [ i ] [ k ] / a [ k ] [ k ], j=k; j<n; j++)
				a [ i ] [ j ]-=M*a [ k ] [ j ];
	}
	//Вычисление определителя.
	for ( i =0; i<n; i++)
		det*=a [ i ] [ i ];
	//Возврат определителя в качестве результата функции
	for ( i =0; i<n; i++)
		delete [ ] a [ i ];
	delete [ ] a;
	return det;
}
int main ( )
{
	int result, i, j,N;
	double **a, b;
	cout<<" N = ";
	cin>>N;
	a=new double _ [N ];
	for ( i =0; i<N; i++)
		a [ i ]=new double [N ];
	//Ввод значений исходной матрицы.
	cout<<"Ввод матрицы A "<<endl;
	for ( i =0; i<N; i++)
		for ( j =0; j<N; j++)
			cin>>a [ i ] [ j ];
	//Обращение к функции вычисления определителя.
	cout<<"определитель= "<<determinant ( a,N)<<endl;
}

Блок-схема алгоритма вычисления определителя


Рис. 6.16.  Блок-схема алгоритма вычисления определителя

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

6.5 Задачи для самостоятельного решения

6.5.1 Основные операции при работе с матрицами

Разработать программу на языке С++ для решения следующей задачи.

  1. В двумерном массиве , состоящем из целых чисел, вычислить:
    • наименьший элемент;
    • сумму положительных элементов;
    • количество простых чисел, расположенных на диагоналях матрицы.
    Для заданной матрицы и матрицы того же типа и размерности найти значение выражения .
  2. В двумерном массиве , состоящем из целых чисел, вычислить:
    • сумму элементов;
    • количество нечётных элементов;
    • минимальное простое число среди элементов, расположенных на главной диагонали.
    Для заданной матрицы и матрицы того же типа и размерности найти значение выражения
  3. В двумерном массиве , состоящем из целых чисел, вычислить:
    • индексы наибольшего элемента;
    • количество отрицательных элементов;
    • среднее геометрическое среди простых чисел, расположенных на побочной диагонали.
    Для заданной матрицы размерности найти значение выражения
  4. В двумерном массиве , состоящем из вещественных чисел, вычислить:
    • сумму элементов;
    • произведение ненулевых элементов;
    • два наибольших значения матрицы.
    Для заданной матрицы и матрицы того же типа и размерности найти значение выражения
  5. В двумерном массиве , состоящем из вещественных чисел, вычислить:
    • произведение элементов;
    • сумму положительных элементов;
    • два наименьших значения среди элементов расположенных по периметру матрицы.
    Для заданной матрицы и матрицы того же типа, но другой размерности найти значение выражения .
  6. В двумерном массиве , состоящем из целых чисел, вычислить:
    • наименьший элемент;
    • количество чётных чисел;
    • сумму положительных элементов, которые представляют собой возрастающую последовательность цифр.
    Для заданной матрицы и матрицы того же типа и размерности найти значение выражения
  7. В двумерном массиве , состоящем из целых чисел, вычислить:
    • индексы наименьшего элемента;
    • сумму квадратов отрицательных элементов;
    • минимальное простое число среди элементов, расположенных в заштрихованной части матрицы (рис. 6.17).
    Для заданной матрицы и матрицы того же типа и размерности найти значение выражения
  8. В двумерном массиве , состоящем из целых чисел, вычислить:
    • среднее арифметическое элементов;
    • наименьший чётный элемент;
    • количество чисел-палиндромов, расположенных в заштрихованной части матрицы (рис. 6.18).
    Для заданной матрицы и матрицы того же типа и размерности найти значение выражения
  9. В двумерном массиве , состоящем из целых чисел, вычислить:
    • среднее геометрическое элементов;
    • наибольший нечётный элемент;
    • количество составных чисел среди элементов, расположенных в заштрихованной части матрицы (рис. 6.19).
    Для заданной матрицы найти значение выражения .
  10. В двумерном массиве , состоящем из целых чисел, вычислить:
    • индексы наименьшего элемента;
    • среднее арифметическое нечётных чисел;
    • количество положительных элементов, которые представляют собой убывающую последовательность цифр.
    Для заданной матрицы найти значение выражения .
  11. В двумерном массиве , состоящем из вещественных чисел, вычислить:
    • среднее арифметическое элементов;
    • элемент наиболее отличающийся от среднего арифметического.
    Отразить заданную матрицу относительно побочной диагонали. Для матрицы и матрицы того же типа и размерности найти значение выражения .
  12. В двумерном массиве , состоящем из целых чисел, вычислить:
    • среднее геометрическое элементов;
    • элемент наименее отличающийся от среднего геометрического;
    • количество положительных элементов с чётной суммой цифр, расположенных в заштрихованной части матрицы (рис. 6.20)
    Для матрицы и матрицы того же типа и размерности найти значение выражения .
  13. В двумерном массиве , состоящем из целых чисел, вычислить:
    • наименьший элемент и его индексы;
    • среднее арифметическое положительных чётных элементов;
    • произведение простых чисел-палиндромов, расположенных в заштрихованной части матрицы (рис. 6.21).
    Для заданной матрицы и матрицы того же типа и размерности найти значение выражения .
  14. В двумерном массиве , состоящем из целых чисел, вычислить:
    • наибольший элемент и его индексы;
    • среднее арифметическое элементов, расположенных на диагоналях матрицы.
    Сформировать новую матрицу , каждый элемент которой будет равен сумме цифр элемента матрицы . Для матриц найти значение выражения .
  15. В двумерном массиве , состоящем из целых чисел, вычислить:
    • произведение элементов;
    • индексы наибольшего чётного элемента;
    • сумму чисел-палиндромов, расположенных вне диагоналей матрицы.
    Для заданной матрицы размерности и матрицы того же типа и размерности найти значение выражения .
  16. В двумерном массиве , состоящем из целых чисел, вычислить:
    • среднее арифметическое элементов;
    • наименьший нечётный элемент, расположенный в заштрихованной части матрицы (рис. 6.22).
    Сформировать новую матрицу , каждый элемент которой равен значению матрицы , цифры которого записаны в обратном порядке. Для матриц и найти значение выражения .
  17. В двумерном массиве , состоящем из целых чисел, вычислить:
    • сумму элементов;
    • количество ненулевых элементов, расположенных по периметру матрицы;
    • среднее геометрическое чисел, в представлении которых все цифры различные.
    Для заданной матрицы и матрицы того же типа и размерности найти значение выражения .
  18. В двумерном массиве , состоящем из целых чисел, вычислить:
    • произведение элементов;
    • сумму элементов, расположенных вне периметра матрицы;
    • наименьшее число, состоящее из одинаковых цифр.
    Для заданной матрицы и матрицы того же типа, но другой размерности найти значение выражения .
  19. В двумерном массиве , состоящем из целых чисел, вычислить:
    • среднее геометрическое элементов;
    • индексы наибольшего чётного элемента, расположенного в заштрихованной части матрицы (рис. 6.23).
    Сформировать новую матрицу , каждый элемент которой равен значению матрицы в восьмеричной системе счисления. Найти значение выражения .
  20. В двумерном массиве , состоящем из целых чисел, вычислить:
    • сумму квадратов элементов;
    • количество совершённых чисел, расположенного в заштрихованной части матрицы (рис. 6.24).
    Сформировать новую матрицу , каждый элемент которой равен количеству делителей соответствующего значения матрицы . Для матриц и найти значение выражения .
  21. В двумерном массиве , состоящем из целых чисел, вычислить:
    • наименьшее абсолютное значение элементов;
    • произведение ненулевых элементов, расположенного в заштрихованной части матрицы (рис. 6.25).
    Сформировать новую матрицу , каждый элемент которой равен количеству цифр в соответствующем элементе матрицы . Найти значение выражения .
  22. В двумерном массиве , состоящем из целых чисел, вычислить:
    • произведение ненулевых элементов;
    • наибольшее абсолютное значение элементов, расположенного в заштрихованной части матрицы (рис. 6.26).
    Сформировать новую матрицу , каждый элемент которой равен значению матрицы в пятеричной системе счисления. Найти значение выражения .
  23. В двумерном массиве , состоящем из вещественных чисел, вычислить:
    • сумму модулей элементов;
    • количество нулевых элементов, расположенных вне периметра матрицы;
    • два наибольших положительных значения.
    Для заданной матрицы и матрицы того же типа, но другой размерности найти значение выражения .
  24. В двумерном массиве , состоящем из вещественных чисел, вычислить:
    • сумму квадратов элемента;
    • индексы первого нулевого элемента матрицы;
    • два наибольших значения, расположенных вне периметра матрицы;
    Для заданной матрицы найти значения выражений и .
  25. В двумерном массиве , состоящем из вещественных чисел вычислить:
    • произведение квадратов элемента;
    • индекс последнего нулевого элемента матрицы;
    • два наименьших значения, расположенных вне диагоналей матрицы.
    Из элементов заданной матрицы сформировать верхнетреугольную матрицу и нижнетреугольную матрицу . Проверить равенство .



Рис. 6.17. 



Рис. 6.18. 



Рис. 6.19. 



Рис. 6.20. 



Рис. 6.21. 



Рис. 6.22. 



Рис. 6.23. 



Рис. 6.24. 



Рис. 6.25. 



Рис. 6.26. 

6.5.2 Работа со строками и столбцами матрицы

Разработать программу на языке С++ для решения следующей задачи.

  1. Задана матрица целых чисел . Сформировать массив , в который записать среднее арифметическое элементов каждого столбца заданной матрицы. Вывести номера строк матрицы, в которых находится более двух простых чисел.
  2. Задана матрица вещественных чисел . Сформировать массив , в который записать среднее геометрическое положительных элементов каждой строки заданной матрицы. Определить количество столбцов, упорядоченных по возрастанию.
  3. Задана матрица целых чисел . Все простые числа, расположенные на побочной диагонали, заменить суммой цифр максимального элемента соответствующей строки матрицы. Сформировать массив , в который записать произведения элементов нечётных строк заданной матрицы.
  4. В матрице целых чисел поменять местами диагональные элементы, упорядоченных по убыванию строк. Сформировать массив , в который записать суммы элементов чётных столбцов заданной матрицы.
  5. Задана матрица целых чисел . Максимальный элемент каждого столбца заменить суммой цифр максимального элемента матрицы. Сформировать массив , в который записать количество чётных элементов в каждой строке заданной матрицы.
  6. Задана матрица целых чисел . Максимальный элемент каждого столбца заменить суммой цифр модуля минимального элемента матрицы. Сформировать массив , в который записать количество нечётных элементов в каждой строке заданной матрицы.
  7. Задана матрица целых чисел . Сформировать массив из максимальных элементов столбцов заданной матрицы. Вывести индексы чиселпалиндромов, которые находятся на диагоналях матрицы.
  8. Задана матрица вещественных чисел . Сформировать массив из номеров столбцов матрицы, в которых есть хотя бы один ноль. Найти строку с максимальной суммой элементов и поменять её с первой строкой.
  9. Задана матрица вещественных чисел . Сформировать вектор из средних арифметических положительных значений строк матрицы, и вектор из номеров столбцов, которые представляют собой знакочередующийся ряд.
  10. В каждом столбце матрицы вещественных чисел заменить минимальный элемент суммой положительных элементов этого же столбца. Сформировать вектор из номеров строк, представляющих собой знакочередующийся ряд.
  11. В матрице целых чисел обнулить строки, в которых более двух простых чисел. Сформировать массив из минимальных значений столбцов матрицы.
  12. В матрице вещественных чисел найти и вывести номера столбцов, упорядоченных по убыванию элементов. Сформировать массив из максимальных значений строк матрицы.
  13. В матрице вещественных чисел найти и вывести номера строк, упорядоченных по возрастанию элементов. Сформировать массив из номеров минимальных и максимальных значений столбцов матрицы.
  14. В матрице вещественных чисел найти и вывести номера столбцов, упорядоченных по возрастанию. Сформировать вектор из номеров минимальных и максимальных значений строк матрицы.
  15. В матрице вещественных чисел найти и вывести номера строк, упорядоченных по убыванию. Сформировать вектор из максимальных и минимальных значений столбцов матрицы.
  16. В матрице вещественных чисел найти максимальный и минимальный элементы. Поменять местами элементы строки с максимальным значением и элементы столбца с минимальным значением.
  17. Задана матрица целых чисел . Сформировать массив , каждый элемент которого равен количеству положительных элементов с чётной суммой цифр в соответствующей строке матрицы. В столбцах матрицы поменять местами наибольший и наименьший элементы.
  18. Задана матрица целых чисел . Сформировать массив , каждый элемент которого равен количеству положительных чисел с суммой цифр, кратной трём в соответствующем столбце матрицы. Найти строку с максимальным произведением элементов.
  19. Задана матрица целых чисел . Все числа-палиндромы, расположенные на главной диагонали, заменить суммой цифр модуля минимального элемента соответствующего столбца матрицы. Сформировать вектор из произведений абсолютных ненулевых значений соответствующих строк матрицы.
  20. Задана матрица целых чисел . Поменять местами элементы на диагоналях в столбцах, упорядоченных по возрастанию модулей. Сформировать вектор , каждый элемент которого равен сумме составных значений в соответствующей строке матрицы.
  21. Задана матрица целых чисел . Минимальный элемент каждой строки заменить суммой цифр максимального простого элемента матрицы. Сформировать вектор , каждый элемент которого — среднее геометрическое ненулевых элементов в соответствующем столбце матрицы.
  22. Задана матрица целых чисел . Максимальный элемент каждого столбца заменить суммой цифр минимального простого элемента матрицы. Сформировать вектор , каждый элемент которого равен количеству чётных элементов в соответствующей строке матрицы.
  23. Задана матрица целых чисел . Обнулить строки, в которых на диагоналях нет чисел-палиндромов. Сформировать вектор , каждый элемент которого равен количеству нечётных элементов в соответствующем столбце матрицы.
  24. Задана матрица вещественных чисел . Найти столбец с минимальным произведением элементов. Поменять местами элементы этого столбца и элементы последнего столбца. Сформировать вектор из сумм квадратов соответствующих строк матрицы.
  25. Задана матрица целых чисел . В каждой строке заменить максимальный элемент суммой цифр минимального элемента этой же строки. Сформировать массив , пара элементов которого равна соответственно количеству чётных и нечётных чисел в соответствующем столбце матрицы.

6.5.3 Решение задач линейной алгебры

Разработать программу на языке С++ для решения следующей задачи.

  1. Задана матрицы и . Вычислить матрицу .
  2. Задан массив . Сформировать матрицы и по формулам: .

    Решить матричное уравнение , где — единичная матрица.

  3. Даны массивы и . Сформировать матрицы и по формулам:.

    Решить матричное уравнение , где — единичная матрица.

  4. Квадратная матрица называется ортогональной, если . Определить, является ли данная матрица ортогональной:
  5. Для матрицы , где — единичная матрица, а , проверить свойство ортогональности: .
  6. Проверить, образуют ли базис векторы

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

    .
  7. Найти вектор как решение данной системы уравнений
    Вычислить модуль вектора .
  8. Вычислить скалярное произведение векторов и . Вектор , а вектор является решением СЛАУ: _
  9. Вычислить вектор , решив СЛАУ
    Найти .
  10. Вычислить вектор , решив СЛАУ
    Найти модуль вектора .
  11. Вычислить угол между векторами и . Вектор является решением СЛАУ:
    .
  12. Решив систему уравнений методом Гаусса:
    Вычислить .
  13. Решить СЛАУ , где |.
  14. Решить СЛАУ , где .
  15. Заданы матрицы и . Найти определитель матрицы .
  16. Задан массив . Сформировать матрицы и по формулам: . Найти определитель .
  17. Для матрицы, где — единичная матрица, а
    проверить свойство . При помощи метода Гаусса решить СЛАУ .
  18. Квадратная матрица является симметричной, если для неё выполняется свойство . Проверить это свойство для матрицы
    . Вычислить . Убедиться, что .
  19. Ортогональная матрица обладает следующими свойствами:
    • модуль определителя ортогональной матрицы равен 1;
    • сумма квадратов элементов любого столбца ортогональной матрицы равна 1;
    • сумма произведений элементов любого столбца ортогональной матрицы на соответствующие элементы другого столбца равна 0.
    Проверить эти свойства для матриц:
    .
  20. Проверить, образуют ли базис векторы

    Если образуют, то найти координаты вектора в этом базисе. Для решения задачи необходимо показать, что определитель матрицы F со столбцами отличен от нуля, а затем вычислить координаты вектора в новом базисе, решив СЛАУ .

  21. Решить СЛАУ:
    Для матрицы проверить условия ортогональности: и .
  22. Найти и для матрицы
  23. Найти для матрицы
  24. Решить СЛАУ методом Гаусса
    Выполнить проверку .
  25. Задан массив . Сформировать матрицы и по формулам

    Решить матричное уравнение , где — единичная матрица.

Лекция 7. Организация ввода-вывода в C++

Рассматриваются конструкции cin и cout, и возможности их использования для организации форматированного ввода-вывода.

7.1 Форматированный ввод-вывод в C++

Для управления вводом-выводом в C++ используются:

7.1.1 Использование флагов форматного ввода-вывода

Флаги позволяют включить или выключить один из параметров вывода на экран. Для установки флага вывода используется следующая конструкция языка C++:

cout.setf(ios::flag)

Для снятия флага применяют конструкцию

cout.unsetf(ios::flag)

здесь flag — имя конкретного флага.

Если при выводе необходимо установить несколько флагов, то можно воспользоваться арифметической операцией "или" (|). В этом случае конструкция языка C++ будет такой:

cout.setf(ios::flag1|ios::flag2|ios::flag3)

В данном случае flag1, flag2, flag3 — имена устанавливаемых флагов вывода.

В табл. 7.1 приведены некоторые флаги форматного вывода с примерами их использования.

Таблица 7.1. Некоторые флаги форматного вывода
ФлагОписаниеПример использования1)Результат
rightВыравнивание по правой границе
int r=-25;
cout. setf ( ios :: right );
cout.width(15);
cout<<"r="<<r<<endl;
r=-25
leftВыравнивание по левой границе (по умолчанию)
double r=-25.45;
cout. setf ( ios :: left );
cout.width(50);
cout<<"r="<<r<<endl;
r=-25.45
boolalphaВывод логических величин в текстовом виде (true, false)
bool a=true;
cout<<a<<endl;
cout. setf ( ios :: boolalpha);
cout<<a<<endl;
true
decВывод величин в десятичной системе счисления (по умолчанию)
int r=-25;
cout<<"r="<<r<<endl;
r=-25
octВывод величин в восьмеричной системе счисления
int p=23;
//Отменить, установленный
по умолчанию, вывод в
десятичной системе счисления
cout.unsetf( ios :: dec);
//Установить вывод в
восьмеричной системе счисления
cout. setf ( ios :: oct);
cout<<"p="<<p<<endl;
p=27
hexВывод величин в шестнадцатеричной системе счисления
int p=23;
//Отменить, установленный
по умолчанию, вывод в
десятичной системе счисления
cout.unsetf( ios :: dec);
//Установить вывод
в шестнадцатеричной
системе счисления
cout. setf ( ios :: hex);
cout<<"p="<<p<<endl;
p=17
showbaseВыводить индикатор основания системы счисления
int p=23;
cout.unsetf( ios :: dec);
cout. setf ( ios :: hex| ios :: showbase);
cout<<"p="<<p<<endl;
p=0x17
uppercaseИспользовать прописные буквы в шестнадцатеричных цифрах
int p=29;
cout.unsetf( ios :: dec);
cout. setf ( ios :: hex| ios :: uppercase);
cout<<"p="<<p<<endl;
p=1D
showposВыводить знак "+" для положительных чисел
int p=29;
cout. setf ( ios :: showpos);
cout<<"p="<<p<<endl;
p=+29
scientificЭкспоненциальная форма вывода вещественных чисел
double p=146.673;
cout. setf ( ios :: scientific );
		cout<<"p="<<p<<endl;
p=1.466730e+002
fixedФиксированная форма вывода вещественных чисел (по умолчанию)
double p=146.673;
cout. setf ( ios :: fixed );
cout<<"p="<<p<<endl;
p=146.673

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

Ещё одним способом форматирования является использование манипуляторов непосредственно в конструкциях cin и cout.

7.1.2 Использование манипуляторов форматирования

Манипуляторы встраиваются непосредственно в операторы ввода-вывода. C одним из манипуляторов () читатель уже встречался начиная с первой главы книги. В табл. 7.2 приведены основные манипуляторы форматирования с примерами их использования. Для корректного использования всех манипуляторов необходимо подключить библиотеку:

#include <iomanip>
Таблица 7.2. Некоторые манипуляторы форматирования
МанипуляторМанипуляторПример использованияРезультат
setw(n)Определяет ширину поля вывода в n символов
int r=253;
cout. setf ( ios :: fixed );
cout<<"r="<<setw(8)<<r<<endl;
r= 253
setprecision(n)Определяет количество цифр в дробной части числа
double h=1234.6578;
cout. setf ( ios :: fixed );
cout<<"h="<<setw(15);
cout<<setprecision(3);
cout<<h<<endl;
h=1234.658
decПеревод числа в десятичную систему (по умолчанию)
int r=0253;
cout<<"r="<<dec<<r<<endl;
; r=171
octПеревод числа в восьмеричную систему
int r=253;
cout<<"r="<<oct<<r<<endl;
r=375
hexПеревод числа в шестнадцатеричную систему
int r=253;
cout<<"r="<<hex<<r<<endl
p=fd
rightВыравнивание по правой границе
int r=-25;
cout.width(15);
cout<<"r="<<setw(15)<<right;
cout<<r<<endl;
r=-25
leftВыравнивание по левой границе (по умолчанию)
int r=-25;
cout.width(15);
cout<<"r="<<setw(15)<<left;
cout<<r<<endl;
1)
r=-25
boolalphaВывод логических величин в текстовом виде (true, false)
bool a=true;
cout<<boolalpha<<a<<endl;
true
noboolalphaВывод логических величин в числовом виде (1, 0)
bool a=true;
cout<<noboolalpha<<a<<endl;
1
showposВыводить знак "+" для положительных чисел
int p=29;
cout<<"p="<<showpos<<p<<
endl;
p=+29
noshowposНе выводить знак "+" для положительных чисел
int p=29;
cout<<"p="<<
noshowpos<<p<<endl;
p=29
uppercaseИспользовать прописные буквы в шестнадцатеричных цифрах
int p=253;
cout<<"p="<<hex<<
uppercase<<p<<endl;
p=FD
nouppercaseИспользовать строчные буквы в шестнадцатеричных цифрах
int p=253;
cout<<"p="<<hex<<nouppercase;
cout<<p<<endl;
p=fd
showbaseВыводить индикатор основания системы счисления
int p=253;
cout<<"p="<<hex<<uppercase
<<showbase<<p<<endl;
p=0XFD
noshowbaseНе выводить индикатор основания системы счисления
int p=253;
cout<<"p="<<hex<<uppercase;
cout<<noshowbase<<p<<endl;
p=FD
setfill(c)Установить символ с как заполнитель
cout<<"x="<<right<<
setw(10)<<setprecision(4);
cout<<setfill("!");
cout<<(float)1/7<<endl;
cout<<"x="<<left<<setw(10);
cout<<setprecision(4);
cout<<setfill("!");
cout<<(float)1/7<<endl;
x=!!!!0.1429 x=0.1429!!!!
scientificЭкспоненциальная форма вывода вещественных чисел
double p=146.673;
cout<<"p="<<scientific<<p<<
endl;
p=1.466730e+002
fixedФиксированная форма вывода вещественных чисел (по умолчанию)
cout<<"p="<<fixed<<p<<endl;
p=146.673

Кроме того, управлять шириной поля вывода можно с помощью операторов:

В п. 7.1.1 и 7.1.2 были рассмотрены основные возможности форматированного ввода-вывода. При использовании конструкций cin и cout фактически происходит ввод-вывод в текстовый файл. При вводе текстовым файлом является клавиатура ПК, при выводе в качестве текстового файла выступает экран дисплея, cin и cout фактически являются именами потоков2), которые отвечают за ввод и вывод в текстовый файл. Поэтому многие рассмотренные возможности форматированного ввода-вывода будут использоваться и при обработке текстовых файлов.

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

7.2 Работа с текстовыми файлами в C++

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

Для работы с файлами используются специальные типы данных, называемые потоками1).Поток ifstream служит для работы с файлами в режиме чтения. Поток ofstream служит для работы с файлами в режиме записи. Для работы с файлами в режиме как чтения, так и записи служит поток iofstream.

В программах на C++ при работе с текстовыми файлами необходимо подключать библиотеки iostream и fstream.

Для того, чтобы записать данные в текстовый файл, необходимо:

  1. Описать переменную типа ofstream.
  2. Открыть файл с помощью функции open.
  3. Вывести информацию в файл.
  4. Закрыть файл.

Для того, чтобы считать данные из текстового файла, необходимо:

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

7.2.1 Запись информации в текстовый файл

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

ofstream F;2)

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

F.open("file", mode);

Здесь F — переменная, описанная как ofstream, file — имя файла на диске, mode — режим работы с открываемым файлом.

Файл может быть открыт в одном из следующих режимов:

Параметр mode может отсутствовать, в этом случае файл открывается в режиме по умолчанию для данного потока4).

После удачного открытия файла (в любом режиме) в переменной F будет храниться true, в противном случае false. Это позволит проверять корректность операции открытия файла.

Открыть файл (в качестве примера возьмём файл abc.txt) в режиме записи можно одним из следующих способов:

//Первый способ.
ofstream F;
f.open ( " abc.txt ", ios::out );
//Второй способ,
//режим ios::out является режимом по умолчанию для потока ofstream
ofstream F;
f.open ( " abc.txt " )
//Третий способ объединяет описание переменной типа поток
//и открытие файла в одном операторе
ofstream F( " abc.txt ", ios::out );

После открытия файла в режиме записи будет создан пустой файл, в который можно будет записывать информацию. Если нужно открыть файл, чтобы в него что-либо дописать, то в качестве режима следует использовать значение ios::app.

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

Например, для записи в поток F переменной a оператор вывода будет иметь вид:

F<<a;

Для последовательного вывода в поток G переменных b, c и d оператор вывода станет таким:

G<<b<<c<<d;

Закрытие потока осуществляется с помощью оператора:

F.close();

В качестве примера рассмотрим следующую задачу.

Задача 7.1. Создать текстовый файл abc.txt и записать туда n вещественных чисел.

Текст программы с комментариями:

#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
int main ( )
{
	int i, n; double a;
	ofstream f; //Описывает поток для записи данных в файл.
	f.open ( " abc.txt " ); //Открываем файл в режиме записи,
		//режим ios::out устанавливается по умолчанию.
	cout<<" n = "; cin>>n; //Ввод количества вещественных чисел.
	for ( i =0; i<n; i++)
	{
		cout<<" a = "; cin>>a; //Ввод очередного числа.
		if ( i<n-1) //Если число не последнее,
			f<<a<<" \t "; //записать в файл это число и символ табуляции, иначе
		else f<<a; //записать только число.
	}
	f.close ( ); //Закрытие потока.
return 0;
}

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

if (i<n-1) f<<a<<"\t"; else f<<a;

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

В результате работы программы будет создан текстовый файл abc.txt, который можно просмотреть средствами обычного текстового редактора (рис. 7.1,рис. 7.2).

7.2.2 Чтение информации из текстового файла

Процесс работы программы к задаче 7.1. Ввод исходных данных.


Рис. 7.1.  Процесс работы программы к задаче 7.1. Ввод исходных данных.

Текстовый файл abc.txt, созданный программой к задаче 7.1.


увеличить изображение

Рис. 7.2.  Текстовый файл abc.txt, созданный программой к задаче 7.1.

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

ifstream F;
F.open("abc.txt", ios::in);

5)

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

Например, для чтения из потока F в переменную a оператор ввода будет иметь вид:

F>>a;

Для последовательного ввода из потока G в переменные b, с и d оператор ввода станет таким:

G>>b>>c>>d;

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

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

F.eof();

Здесь F — имя потока, функция возвращает логическое значение: true — если достигнут конец файла, если не достигнут функция возвращает значение false.

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

while ( ! F.eof ( ) ) //Организован цикл, условием окончания цикла
//является достижение конца файла, в этом случае F.eof() вернёт true.
{
	F>>a; //Чтение очередного значения из потока F в переменную a.
	...
	<обработка значения переменной a>
}

Рассмотрим следующую задачу.

Задача 7.2. В текстовом файле хранятся вещественные числа (рис. 7.2), вывести их на экран и вычислить их количество.

Текст программы с комментариями приведён ниже.

#include <iostream>
#include <fstream>
using namespace std;
int main ( )
{
	ifstreamf; //Поток для чтения.
	float a; int n=0;
	f.open ( " abc.txt " ); //Открываем файл в режиме чтения.
	if ( f ) //Если открытие файла прошло корректно, то
	{
		while ( ! F.eof ( ) ) //Организован цикл, выполнение цикла
		//прервётся, когда будет достигнут конца файла.
		{
			f>>a; //Чтение очередного значения из потока f в переменную a.
			cout<<a<<" \t "; //Вывод значения переменной a
			n++; //Увеличение количества считанных чисел.
		}
		f.close ( ); //Закрытие потока.
		cout<<" n = "<<n<<endl; //Вывод на экран количества чисел.
	}
	else cout<<"Файл не найден"<<endl; //Если открытие файла прошло некорректно, то
	//вывод сообщения, об отсутствии такого файла.
	return 0;
}

Результат работы программы к задаче 7.2:

3.14159 2.789 -21.14 543.89 -90.1 n=5

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

3.14159 2.789 -21.14 543.89 -90.1 -90.1 n= 6

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

while ( ! F.eof ( ) )
{
	f>>a;
	if ( ! F.eof ( ) ) //Проверяем, достигнут ли конец файла, если нет — печатаем значение a
	{
	cout<<a<<" \t ";
	n++;
	}
}

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

#include <iostream>
#include <fstream>
using namespace std;
int main ( )
{
	ifstreamf;
	float a; int i, n=5;
	f.open ( " abc.txt " );
	if ( f )
	{
		for ( i =1; i<=n; f>>a, cout<<a<<" \t ", i++);
			f.close ( );
	}
	else cout<<"Файл не найден"<<endl;
	return 0;
}

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

Задача 7.3. В файле (рис. 7.2) хранится массив вещественных чисел, дописать в файл этот же массив, упорядочив его по возрастанию.

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

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

Текст программы с комментариями приведён ниже.

#include <iostream>
#include <fstream>
using namespace std;
int main ( )
{
	ifstreamf; //Поток для чтения.
	ofstream g; //Поток для записи.
	float *a, b;
	a=new float [ 100 ];
	int i, j, n=0;
	f.open ( " abc.txt ", ios::in ); //Открываем файл в режиме чтения.
	if ( f )
	{
		while ( ! F.eof ( ) )
		{
			f>>a [ n ];
			n++;
		}
		//Сортировка массива.
		for ( i =0; i<n-1; i++)
			for ( j =0; j<n-i -1; j++)
			if ( a [ j ]>a [ j +1 ])
			{
				b=a [ j ];
				a [ j ]=a [ j + 1 ];
				a [ j +1]=b;
			}
		f.close ( ); //Закрываем поток для чтения.
		g.open ( " abc.txt ", ios::app ); //Открываем поток для того, чтобы дописать данные.
		g<<" \n "; //Запись в файл символа конца строки
		for ( i =0; i<n; i++) //Запись в файл
		if ( i<n-1) g<<a [ i ]<< " \t "; //элемента массива и символа табуляции.
		else g<<a [ i ]; //Запись последнего элемента массива
		g.close ( ); //Закрытие файла.
	}
	else cout<<"Файл не найден"<<endl;
	delete [ ] a;
	return 0;
}

Содержимое файла abc.txt после запуска программы к задаче 7.3

3.14159 2.798 -21.14 543.89 -90.1
-90.1 -21.14 2.798 3.14159 543.89

7.3 Обработка двоичных файлов

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

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

  1. Описать файловую переменную с помощью оператора FILE *filename; Здесь filename — имя переменной, где будет храниться указатель на файл.
  2. Открыть файл с помощью функции fopen.
  3. Записать информацию в файл с помощью функции fwrite.
  4. Закрыть файл с помощью функции fclose.

Для того, чтобы считывать данные из двоичного файла, необходимо:

  1. Описать переменную типа FILE *.
  2. Открыть файл с помощью функции fopen.
  3. Считать необходимую информацию из файла с помощью функции fread, при считывании информации следить за тем, достигнут ли конец файла.
  4. Закрыть файл с помощью функции fclose.

Рассмотрим основные функции, необходимые для работы с двоичными файлами.

Для открытия файла предназначена функция:

FILE *fopen(const *filename, const char *mode); где, filename — строка, в которой хранится полное имя открываемого файла, mode — строка, которая определяет режим работы с файлом; возможны следующие значения:

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

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

Для закрытия файла предназначена функция

int fclose(FILE *filename);

Она возвращает 0 при успешном закрытии файла и NULL в противном случае.

Для удаления файлов существует функция

int remove(const char *filename);

Эта функция удаляет с диска файл с именем filename. Удаляемый файл должен быть закрыт. Функция возвращает ненулевое значение, если файл не удалось удалить.

Для переименования файлов предназначена функция

int rename(const char *oldfilename, const char *newfilename);

здесь первый параметр — старое имя файла, второй — новое. Возвращает 0 при удачном завершении программы.

Чтение из двоичного файла осуществляется с помощью функции

fread (void *ptr, size, n, FILE *filename)

Эта функция считывает из файла filename в массив ptr n элементов размера size. Функция возвращает количество считанных элементов. После чтения из файла указатель файла смещается на n*size байт.

Запись в двоичный файл осуществляется с помощью функции

fwrite (const void *ptr, size, n, FILE *filename);

Функция записывает в файл filename из массива ptr n элементов размера size. Функция возвращает количество записанных элементов. После записи информации в файл, указатель файла смещается на n*size байт.

Для контроля достижения конца файла есть функция

int feof(FILE * filename);

Она возвращает ненулевое значение, если достигнут конец файла.

Рассмотрим использование двоичных файлов на примере решения двух стандартных задач.

Задача 7.4.Создать двоичный файл , куда записать целое число и вещественных чисел.

#include <iostream>
#include <fstream>
using namespace std;
int main ( )
{
	FILE * f; //Описание файловой переменной.
	int i, n; double a;
	f=fopen ( " abc.dat ", " wb " ); //Создание двоичного файла в режиме записи.
	cout<<" n = "; cin>>n; //Ввод числа n.
	fwrite (&n, sizeof ( int ), 1, f ); //Запись числа в двоичный файл.
	for ( i =0; i<n; i++) //Цикл для ввода n вещественных чисел.
	{
		cout<<" a = "; cin>>a; //Ввод очередного вещественного числа.
		fwrite (&a, sizeof ( double ), 1, f ); //Запись числа в двоичный файл.
	}
	fclose ( f ); //Закрыть файл.
	return 0;
}

Задача 7.5. Вывести на экран содержимое созданного в задаче 7.4 двоичного файла abc.dat.

#include <iostream>
#include <fstream>
using namespace std;
int main ( )
{
	FILE * f; //Описание файловой переменной.
	int i, n; double *a;
	f=fopen ( " abc.dat ", " rb " ); //Открыть существующий двоичный файл в режиме чтения.
	fread (&n, sizeof ( int ), 1, f ); //Читать из файла целое число в переменную n.
	cout<<" n = "<<n<<" \n "; //Вывод n на экран.
	a=new double [ n ]; //Выделение памяти для массива из n чисел.
	fread ( a, sizeof ( double ), n, f ); //Чтение n вещественных чисел из файла в массив a.
	for ( i =0; i<n; cout<<a [ i ]<<" \t ", i++); //Вывод массива на экран.
	cout<<endl;
	fclose ( f ); //Закрыть файл.
	return 0;
}

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

FILE * f;
int i, n; double a;
f=fopen ( " file.dat ", " rb " );
for ( i =0; i <15; fread (&a, sizeof ( double ), 1, f ), i++);
fclose ( f );
f=fopen ( " file.dat ", " rb " );
fread (&a, sizeof ( double ), 1, f );
fclose ( f );

Как видно, такое чтение чисел из файла, а затем повторное открытие файла — не самый удачный способ. Гораздо удобнее использовать функцию перемещения указателя файла к заданному байту:

int fseek(FILE *F, long int offset, int origin);

Функция устанавливает указатель текущей позиции файла F, в соответствии со значениями начала отсчёта origin и смешения offset. Параметр offset равен количеству байтов, на которые будет смещён указатель файла относительно начала отсчёта, заданного параметром origin. Если значение offset положительно, то указатель файла смещается вперёд, если отрицательно — назад. Параметр origin должен принимать одно из следующих значений, определённых в заголовке stdio.h:

Функция возвращает нулевое значение при успешном выполнении операции и ненулевое, если возник сбой при выполнении смещения.

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

Задача 7.6. В созданном в задаче 7.4 двоичном файле поменять местами наибольшее и наименьшее из вещественных чисел.

Алгоритм решения задачи состоит из следующих этапов:

Ниже приведён текст программы решения задачи с комментариями.

#include <iostream>
#include <fstream>
using namespace std;
int main ( )
{
	FILE * f; //Описание файловой переменной.
	int i, n, imax, imin;
	double *a, max, min;
	f=fopen ( " abc.dat ", " rb + " ); //Открыть файл в режиме чтения и записи.
	fread (&n, sizeof ( int ), 1, f ); //Считать из файла в переменную n количество элементов в
	файле.
	a=new double [ n ]; //Выделить память для хранения вещественных чисел,
	//эти числа будут хранится в массиве a.
	fread ( a, sizeof ( double ), n, f ); //Считать из файла в массив a вещественные числа.
	//Поиск максимального, минимального элемента в массиве a, и их индексов.
	for ( imax=imin =0, max=min=a [ 0 ], i =1; i<n; i++)
	{
		if ( a [ i ]>max)
		{
			max=a [ i ];
			imax= i;
		}
		if ( a [ i ]<min )
		{
			min=a [ i ];
			imin= i;
	}
	}
	//Перемещение указателя к максимальному элементу.
	fseek ( f, sizeof ( int )+imax* sizeof ( double ),SEEK_SET);
	//Запись min вместо максимального элемента файла.
	fwrite (&min, sizeof ( double ), 1, f );
	//Перемещение указателя к минимальному элементу.
	fseek ( f, sizeof ( int )+imin * sizeof ( double ),SEEK_SET);
	//Запись max вместо минимального элемента файла.
	fwrite (&max, sizeof ( double ), 1, f );
	//Закрытие файла.
	fclose ( f );
	//Освобождение памяти, выделенной под массив a.
	delete [ ] a;
	return 0;
}

7.4 Функции fscanf() и fprintf()

Чтение и запись данных в файл можно выполнять с помощью функций fscanf() и fprintf(). Эти функции подобны функциям scanf() и printf(), описанным в п. 2.9, за тем исключением, что работают не с клавиатурой и экраном, а с файлами. Функции имеют следующие прототипы.

Функция чтения

fscanf(указатель на файл, строка форматов, адреса переменных);

Функция записи

fprintf(указатель на файл,строка форматов, список переменных);

Далее приведён фрагмент программного кода, который демонстрирует пример записи информации в файл my.txt.

char fio [ 30 ]= "Махарадзе В.";
int a=5, b=5, c =4;
float s= ( float ) ( a+b+c ) / 3;
FILE  *f;
f=fopen ( " my.txt ", " w " );
fprintf ( f, "Оценки студента % s \n ", fio );
fprintf ( f, "математика % d, физика % d, химия % d \n ", a, b, c );
fprintf ( f, "Средний балл = %.2 f \n ", s );
fprintf ( f, " \n " );
fclose ( f );

В результате будет сформирован текстовый файл:

Оценки студента Махарадзе В.
математика 5, физика 5, химия 4
Средний балл = 4.67

Рассмотрим пример чтения данных из файла. Пусть в файле test.txt хранится информация:

1 Иванов Пётр 170 78.1
2 Петров Иван 180 89.6
3 Карпов Борис 167 56.7

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

int i, nom;
float Ves;
int Rost;
char fio [ 15 ], name [ 15 ];
FILE * f;
f=fopen ( " test.txt ", " r " );
for ( i =0; i <3; i++)
{
	//Чтение из файла
	fscanf ( f, " % d % s % s % d % f \n ",&nom, &fio,&name,&Rost,&Ves );
	//Вывод на экран
	printf ( " % d % s % s % d %.2 f \n ",nom, fio, name, Rost, Ves );
}
fclose ( f );

Лекция 8. Строки в языке C++

В главе дано общее представление о строках в C++. Описана их структура, способы инициализации, возможности ввода-вывода, приведены примеры обработки строк и текстов.

8.1 Общие сведения о строках в C++

Строка — последовательность символов. Для работы с символами в языке C++ предусмотрен тип данных char. Если в выражении встречается одиночный символ, он должен быть заключён в одинарные кавычки. При использовании в выражениях строка заключается в двойные кавычки. Признаком конца строки является нулевой символ "\0". В C++ строки можно описать с помощью массива символов (массив элементов типа char), в массиве следует предусмотреть место для хранения признака конца строки ("\0").

Например,

char s [ 25 ]; //Описана строка из 25 символов.
//Элемент s[25] предназначен для хранения символа конца строки.
char s [ 15 ]= "Привет"; //Описана строка из 15 символов и ей присвоено значение.
//Определён массив из 3 строк по 30 байт в каждой.
char m[ 3 ] [ 30 ] = { "Пример ", "использования", " строк"}

Для работы со строками можно использовать указатели (char *). Адрес первого символа будет начальным значением указателя.

Рассмотрим пример объявления и ввода строк.

#include <iostream>
using namespace std;
int main ( )
{
char s2 [ 2 5 ], *s3, s4 [ 30 ]; //Описываем 3 строки, s3 — указатель.
cout<<"Введите строку:"<<endl;
cout<<" s2 = "; cin>>s 2; //Ввод строки s2.
cout<<"Была введена строка:"<<endl;
cout<<" s2 = "<<s2<<endl;
s3=s4; //Запись в s3 адреса строки s4. Теперь в указателях s3 и s4 хранится один адрес.
cout<<"Введите строку:"<<endl;
cout<<" s3 = "; cin>>s 3; //Ввод строки s3.
cout<<"Была введена строка:"<<endl;
cout<<" s3 = "<<s3<<endl; //Вывод на экран s3 и s4,
cout<<"Сформирована новая строка:"<<endl;
cout<<" s4 = "<<s4<<endl; //s3 и s4 — одно и тоже.
return 0;
}

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

Введите строку:
s2=Привет!
Была введена строка:
s2=Привет!
Введите строку:
s3=Программируем?
Была введена строка:
s3=Программируем?
Сформирована новая строка:
s4=Программируем?

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

Введите строку:
s2=Привет, Вася!
Была введена строка:
s2=Привет,
Введите строку:
s3=Была введена строка:
s3=Вася!
Сформирована новая строка:
s4=Вася!

Дело в том, что функция cin вводит строки до встретившегося пробела. Более универсальной функцией является функция

cin.getline(char *s, int n);

она предназначена для ввода с клавиатуры строки s с пробелами, причём в строке не должно быть более n символов. Например,

char s [ 25 ];
cout<<"Введите строку:"<<endl;
cout<<" s2 = "; cin.getline ( s, 25 );
cout<<"Была введена строка:"<<endl;
cout<<" s2 = "<<s2<<endl;

Результат:

Введите строку:
s2=Привет, Вася!
Была введена строка:
s2=Привет, Вася!

8.2 Операции над строками

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

Таблица 8.1. Функции работы со строками, библиотека string.h
Прототип функцииОписание функцииПример использованияРезультат
size_t strlen (const char *s)Вычисляет длину строки s в байтах
char s [80];
cout<<"s=";
cin.getline (s,80);
cout<<"s="<<s<<endl
<<"Длина строки\t"<<
strlen(s)<<endl;
s=Hello, Russia!
s=Hello, Russia!
Длина строки 14
char _strcat(char *dest, const char *scr)Присоединяет строку src в конец строки dest, полученная строка возвращается в качестве результата
char s1[80],s2 [80];
cout<<"s1=";
cin.getline (s1,80);
cout<<"s2=";
cin.getline (s2,80);
cout<<"s="<<strcat(s1
,s2);
s1=Hello,
s2=Russia!
s=Hello, Russia!
char *strcpy(char *dest, const char *scr)Копирует строку src в место памяти, на которое указывает dest
char s1[80],s2 [80];
cout<<"s1=";
cin.getline (s1,80);
strcpy(s2,s1);
cout<<"s2="<<s2;
s1=Hello,Russia!
s2=Hello,Russia!
char *strncat(char *dest, const char *dest, size_t maxlen)Присоединяет строку maxlen символов строки src в конец строки dest
char s1[80],s2 [80];
cout<<"s1=";
cin.getline (s1,80);
cout<<"s2=";
cin.getline (s2,80);
cout<<"s="<<strncat(
s1,s2,6);
s1=Hello,
s2=Russia!
s=Hello, Russia
char *strncpy(char *dest, const char *scr, size_t maxlen)Копирует maxlen символов строки src в место памяти, на которое указывает dest
char s1[80],s2 [80];
cout<<"s1=";
cin.getline (s1,80);
strncpy(s2,s1,5);
cout<<"s2="<<s2;
s1=Hello,Russia!
s2=Hello
int strcmp(const char *s1, const char *s2)Сравнивает две строки в лексикографическом порядке с учётом различия прописных и строчных букв, функция возвращает 0, если строки совпадают, возвращает -1, если s1 располагается в упорядоченном по алфавиту порядке раньше, чем s2, и 1 в противоположном случае.
char s1[80],s2 [80];
cout<<"s1=";
cin.getline (s1,80);
cout<<"s2=";
cin.getline (s2,80);
cout<<strcmp(s1,s2)<<
endl;
s1=RUSSIA
s2=Russia
-1
int strncmp(const char *s1, const char *s2, size_t maxlen)Сравнивает maxlen символов двух строк в лексикографическом порядке, функция возвращает 0, если строки совпадают, возвращает -1, если s1 располагается в упо-рядоченном по алфавиту по-рядке раньше, чем s2, и 1 — в противоположном случае.
char s1[80],s2 [80];
cout<<"s1=";
cin.getline (s1,80);
cout<<"s2=";
cin.getline (s2,80);
cout<<strncmp(s1,s2,6)
;
s1=Hello,Russia!
s2=Hello,
0
Таблица 8.2. Функции работы со строками, библиотека stdlib.h
Прототип функцииОписание функцииПример использованияРезультат
double atof(const char*s)Преобразует строку в вещественное число, в случае неудачного преобразова-ния возвращается число 0.0
char a[10];
cout<<"a=";
cin>>a;
cout<<"a="<<atof(a)
<<endl;
a=23.57
a=23.57
int atoi(const char*sПреобразует строку в целое число, в случае неудачного преобразования возвращается число 0
char a[10];
cout<<"a=";
cin>>a;
cout<<"a="<<atoi(a)
<<endl;
a=23
a=23
long atol(const char*s)Преобразует строку в длинное целое число, в случае неудачного преобразования возвращается число 0
char a[10];
cout<<"a=";
cin>>a;
cout<<"a="<<atol(a)
<<endl;
a=23
a=23

Для преобразования числа в строку можно воспользоваться функцией sprintf из библиотеки stdio.h.

sprintf(s, s1, s2);

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

Например, в результате работы следующих команд

char str[80];
sprintf (str, "%s %d %s", "С Новым ", 2014, "годом!!!");

в переменную str будет записана строка "С Новым 2014 годом!!!".

8.3 Тип данных string

Кроме работы со строками как с массивом символов, в C++ существует специальный тип данных string. Для ввода переменных этого типа можно использовать cin1) или специальную функцию:

getline(cin,s);

Здесь s — имя вводимой переменной типа string.

При описании переменной типа string можно сразу присвоить ей значение:

string имя_переменной(s);

Здесь имя_переменной — идентификатор типа string, s — строковая константа. Например, команда

string v("Hello");

создаёт строку v, в которую записывается значение Hello.

Доступ к -му элементу строки осуществляется стандартным образом:

имя_строки[номер_элемента];

Над строками типа string определены следующие операции:

При обработке строк типа string можно использовать следующие функции2):

Задача 8.1. Некоторый текст хранится в файле text.txt. Подсчитать количество строк и слов в тексте.

Предлагаем читателю самостоятельно разобраться в приведённом программном коде.

#include <iostream>
#include <fstream>
#include < std lib .h>
#include <iomanip>
using namespace std;
int main ( )
{
	if streamf;
	int p, j, i, kol,m, n=0;
	stringS [ 10 ];
	f.open ( " text.txt " );
	if ( f )
	{
		while ( ! F.eof ( ) )
		{
			getline ( f, S [ n ] );
			cout<<S [ n]<<" \n ";
			n++;
		}
		f.close ( );
		cout<<endl;
		cout<<"Количество строк в тексте - "<<n<<endl;
		for ( kol =0, i =0; i<n; i++)
		{
			m=S [ i ].length( );
			S [ i ]+=" ";
			for ( p=0;p<m; )
			{
				j=S [ i ].find ( " ", p );
				if ( j !=0) { kol++; p=j +1;}
				else break;
			}
		}
	cout<<"Количество слов в тексте - "<<kol<<endl;
	}
	else cout<<"Файл не найден"<<endl;
	return 0;
}

Результаты работы программы:

Если видим, что с картины
Смотрит кто-нибудь на нас,
Или принц в плаще старинном,
Или в робе верхолаз,
Лётчик или балерина,
Или Колька, твой сосед,
Обязательно картина
Называется портрет.

Количество строк в тексте - 8
Количество слов в тексте - 29

8.4 Задачи для самостоятельного решения

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

  1. Подсчитать количество слов в каждой строке текста.
  2. Подсчитать количество символов в тексте.
  3. Подсчитать количество точек в тексте.
  4. Подсчитать количество пробелов в тексте.
  5. Удалить из теста все пробелы.
  6. Удалить из теста все точки.
  7. Вставить вместо каждого пробела восклицательный знак.
  8. Вставить перед каждым восклицательным знаком вопросительный.
  9. Определить, содержит ли текст хотя бы один восклицательный знак, и в какой строке.
  10. Подсчитать количество слов в чётных строках текста.
  11. Найти номер самой длинной строки текста.
  12. Променять местами первую и последнюю строки текста.
  13. Определить, есть ли в тексте пустые строки.
  14. Определить, содержит ли текст хотя бы пару соседних одинаковых строк.
  15. Найти самую короткую строку текста и заменить её фразой "С новым годом!".
  16. Найти самую длинную строку текста и заменить её пустой строкой.
  17. Определить количество слов в нечётных строках текста.
  18. Определить количество пробелов в чётных строках текста.
  19. Определить количество предложений в тексте, учитывая, что предложение заканчивается точкой, вопросительным или восклицательным знаком.
  20. Поменять местами самую длинную и самую короткую строки текста.
  21. Вывести на печать первое предложение текста, учитывая, что оно заканчивается точкой.
  22. Определить количество пробелов в нечётных строках текста.
  23. Удалить из теста все восклицательные и вопросительные знаки.
  24. Определить, содержит ли текст хотя бы один вопросительный знак, и в какой строке.
  25. Добавить в начало каждой строки текста её номер и пробел.

Лекция 9. Структуры в языке C++

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

Описана библиотека языка C++, позволяющая работать с комплексными числами

9.1 Общие сведения о структурах

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

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

struct student
{
//Поля структуры:
char fio [ 30 ];
char group [ 8 ];
int year;
int informatika, math, fizika, history;
}

На основании созданного структурного типа данных можно описать переменные типа student:

student Vasya; //Переменная Vasya типа student.
student ES [ 50 ]; //Массив, элементы которого имеют тип student.
student *x; //Указатель на тип данных student

Обращаются к полям переменной структурного типа так:

имя_структуры.поле

Например,

Vasya.year; //Обращение к полю year переменной Vasya.
ES[4].math; //Обращение к полю math элемента ES[4].

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

Напомним, что комплексные числа это числа вида , где и — действительные числа, а — мнимая единица, . Комплексное число расширяет понятие действительного числа. Если действительное число — это любая точка на числовой прямой, то под комплексным числом понимают точку на плоскости (рис. 9.1). Модуль комплексного числа вычисляют по формуле .

Для решения задачи 9.1 разработаны две программы. Первая создаёт файл исходных данных, вторая получает из него информацию и обрабатывает её в соответствии с поставленной задачей.

Далее приведён текст программы создания двоичного файла с комплексными числами. В файл complex.dat будет записано число n, а затем последовательно комплексные числа.

#include <iostream>
#include <fstream>
using namespace std;
int main ( )
{
	//Структура Комплексное число.
	struct complex
	{
		//Поля структуры:
		double Re; //Действительная часть.
		double Im; //Мнимая часть.
	};
	complex p; //Переменная для хранения комплексного чисела.
	int i, n;
	FILE *f;
	cout<<" n = "; cin>>n;
	f=fopen ( " complex.dat ", " wb " );
	fwrite (&n, sizeof ( int ), 1, f );
	for ( i =0; i<n; i++)
	{
		cout<<"Введите комплексное число\n ";
		//Ввод комплексного числа:
		cin>>p.Re; //действительная часть,
		cin>>p.Im; //мнимая часть.
		//Вывод комплексного числа.
		cout<<p.Re<<" + "<<p.Im<<" i "<<endl;
		//Запись комплексного числа в двоичный файл.
		fwrite (&p, sizeof ( complex ), 1, f );
	}
	fclose ( f );
	return 0;
}

Геометрическая модель комплексного числа a + b · i


Рис. 9.1.  Геометрическая модель комплексного числа a + b · i

Геометрическая интерпретация комплексно-сопряжённого числа


Рис. 9.2.  Геометрическая интерпретация комплексно-сопряжённого числа

Следующая программа считывает информацию из файла complex.dat — количество комплексных чисел в переменную n, а сами комплексные числа в массив p. Затем происходит поиск комплексного числа с максимальным модулем в массиве p.

#include <iostream>
#include <math.h>
using namespace std;
int main ( )
{
	struct complex
	{
		double Re;
		double Im;
	};
	complex *p;
	int i, n, nmax;
	double max;
	FILE *f;
	f=fopen ( " complex.dat ", " rb " );
	fread (&n, sizeof ( int ), 1, f );
	p=new complex [ n ];
	fread ( p, sizeof ( complex ), n, f );
	//Поиск комплексного числа с максимальным модулем
	max=sqrt ( p[0].Re*p[0].Re+p[0].Im*p[0].Im );
	for ( i =1,nmax=0; i<n; i++)
		if ( sqrt ( p[i].Re*p[i].Re+p[i].Im*p[i].Im )>max)
		{
			max=sqrt ( p[i].Re*p[i].Re+p [i].Im*p[i].Im );
			nmax= i;
		}
	cout<<" max = "<<max<<" \t nmax = "<<nmax<<endl;
	fclose ( f );
	return 0;
}

Задача 9.2. Даны два комплексных числа и . Выполнить над ними основные операции:

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

Разностью двух комплексных чисел и называется комплексное число .

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

Частным двух комплексных чисел и называется комплексное число

Числом, сопряжённым комплексному числу , называется число (рис. 9.2).

Всякое комплексное число, записанное в алгебраической форме , можно записать в тригонометрической или в показательной форме , где — модуль комплексного числа — его аргумент (рис. 9.2).

Для возведения в степень комплексного числа, записанного в тригонометрической форме ), можно воспользоваться формулой Муавра

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

Далее приведён текст программы, реализующий алгоритм решения задачи 9.2. В программе описаны две структуры для работы с комплексными числами: структура complex1 для представления комплексных чисел в алгебраической форме (Re — действительная часть комплексного числа, Im — его мнимая часть) и структура complex2 для представления комплексных чисел в показательной или тригонометрической форме (Modul — модуль комплексного числа, Argum — его аргумент). Кроме того в программе созданы функции, реализующие основные действия над комплексными числами, переход между различными формами представления комплексных чисел, а также ввод-вывод комплексных чисел.

#include <iostream>
#include <math.h>
using namespace std;
struct complex1
{
	float Re;
	float Im;
};
struct complex2
{
	float Modul;
	float Argum;
};
//Ввод числа в алгебраической форме
complex1 vvod1 ( )
{
	complex1 temp;
	cout<<"Введите действительную часть числа\n ";
	cin>>temp.Re;
	cout<<"Введите мнимую часть комплексного числа\n ";
	cin>>temp.Im;
	return temp;
}
//Ввод числа в тригонометрической или показательной форме
complex2 vvod2 ( )
{
	complex2 temp;
	cout<<"Введите модуль комплексного числа\n ";
	cin>>temp.Modul;
	cout<<"Введите аргумент комплексного числа\n ";
	cin>>temp.Argum;
	return temp;
}
//Вывод числа в алгебраической форме
void vivod ( complex1 chislo )
{
	cout<<chislo.Re;
	if ( chislo.Im>=0)
		cout<<" + "<< chislo.Im<<" i "<<endl;
	else
		cout<<" "<< chislo.Im<<" i "<<endl;
}
//Вывод числа в тригонометрической форме
void vivod ( complex2 chislo )
{
	cout<<chislo.Modul<<" ( cos ( "<< chislo.Argum<<" ) + i sin ( "<< chislo.Argum<<
	" ) ) "<<endl;
}
//Перевод числа из тригонометрической формы в алгебраическую,
//pr определяет, выводить или нет полученное число на экран.
complex1 perevod ( complex2 chislo, bool pr=false )
{
	complex1 temp;
	temp.Re=chislo.Modul*cos ( chislo.Argum );
	temp.Im=chislo.Modul*sin ( chislo.Argum );
	if ( pr ) vivod ( temp );
	return temp;
}
//Перевод числа из алгебраической формы в тригонометрическую,
//pr определяет, выводить или нет полученное число на экран.
complex2 perevod ( complex1 chislo, bool pr=false )
{
	complex2 temp;
	temp.Modul=sqrt ( chislo.Re* chislo.Re+
	chislo.Im*chislo.Im );
	temp.Argum=atan ( chislo.Im/ chislo.Re );
	if ( pr ) vivod ( temp );
	return temp;
}
//Функция сложения двух чисел в алгебраической форме,
//pr определяет, выводить или нет число на экран.
complex1 plus1 ( complex1 chislo1, complex1 chislo2, bool pr=true )
{
	complex1 temp;
	temp.Re=chislo1.Re+chislo2.Re;
	temp.Im=chislo1.Im+chislo2.Im;
	if ( pr ) vivod ( temp );
	return temp;
}
//Функция вычитания двух чисел в алгебраической форме,
//pr определяет, выводить или нет число на экран.
complex1 minus1 ( complex1 chislo1, complex1 chislo2, bool pr=true )
{
	complex1 temp;
	temp.Re=chislo1.Re-chislo2.Re;
	temp.Im=chislo1.Im-chislo2.Im;
	if ( pr ) vivod ( temp );
	return temp;
}
//Функция умножения двух чисел в алгебраической форме,
//pr определяет, выводить или нет число на экран.
complex1 mult1 ( complex1 chislo1, complex1 chislo2, bool pr=true )
{
	complex1 temp;
	temp.Re=chislo1.Re* chislo2.Re-chislo1.Im* chislo2.Im;
	temp.Im=chislo1.Im* chislo2.Re+chislo1.Re* chislo2.Im;
	if ( pr ) vivod ( temp );
	return temp;
}
//Функция деления двух чисел в алгебраической форме,
//pr определяет, выводить или нет число на экран.
complex1 divide1 ( complex1 chislo1, complex1 chislo2, bool pr=true )
{
	complex1 temp;
	temp.Re=( chislo1.Re* chislo2.Re+chislo1.Im* chislo2.Im ) / ( chislo2.Re* chislo2 .
	Re+chislo2.Im* chislo2.Im );
	temp.Im=( chislo1.Im* chislo2.Re-chislo1.Re* chislo2.Im ) / ( chislo2.Re* chislo2 .
	Re+chislo2.Im* chislo2.Im );
	if ( pr ) vivod ( temp );
	return temp;
}
//Функция возведения комплексного числа в алгебраической форме
//в целую степень n, pr определяет, выводить или нет полученное число на экран.
complex1 pow1 ( complex1 chislo1, int n, bool pr=true )
{
	complex1 temp;
	complex2 temp2;
	float p=1;
	int i =1;
	temp2=perevod ( chislo1, true ); //Перевод числа в тригонометрическую форму.
	for (; i<=n; p*=temp2.Modul, i++);
	temp.Re=p*cos ( n*temp2.Argum );
	temp.Im=p*sin ( n*temp2.Argum );
	if ( pr ) vivod ( temp );
	return temp;
}
//Функция извлечения корня степени n из комплексного числа
//в алгебраической форме, pr определяет, выводить или нет
//полученные значения на экран. Функция возвращает ro и fi.
void sqrt n 1 ( complex1 chislo1, int n, float _ ro, float _ f i, bool pr=true )
{
	complex1 temp;
	complex2 temp2;
	int i =0;
	temp2=perevod ( chislo1, true ); //Перевод числа в тригонометрическую форму.
	* r o=pow ( temp2.Modul, ( float ) 1/n );
	* f i=temp2.Argum;
	if ( pr )
	{
		for ( i =0; i<n; i++)
		{
			cout<<i<<"-е значение корня\n ";
			temp.Re=*ro* cos ( ( * fi +2*M_PI* i ) /n );
			temp.Im=*ro* sin ( ( * fi +2*M_PI* i ) /n );
			vivod ( temp );
		}
	}
}
int main ( )
{
	complex1 chislo1, chislo2; //Описание комплексных
	complex1 chislo5; //чисел в алгебраической форме.
	complex2 chislo3, chislo4; //Описание комплексных чисел в тригонометрической форме.
	float ro1, fi1;
	chislo1=vvod1 ( ); //Ввод исходных данных
	chislo2=vvod1 ( ); //в алгебраической форме.
	vivod ( chislo1 ); //Вывод исходных данных
	vivod ( chislo2 ); //в алгебраической форме.
	chislo 3=perevod ( chislo1, true ); //Перевод чисел
	chislo 4=perevod ( chislo2, true ); //в тригонометрическую форму и вывод их на экран.
	cout<<"Сумма чисел ";
	chislo5=plus1 ( chislo1, chislo2, true );
	cout<<"Разность чисел ";
	chislo5=minus1 ( chislo1, chislo2, true );
	cout<<"Произведение чисел ";
	chislo5=mult1 ( chislo1, chislo2, true );
	cout<<"Частное чисел ";
	chislo5=divide1 ( chislo1, chislo2, true );
	chislo5=pow1 ( chislo1, 5, true ); //Возведение числа в пятую степень.
	sqrtn1 ( chislo1, 5, &Ro1, &fi1, true ); //Извлечение корня пятой степени.
	return 0;
}

Результаты работы программы к задаче 9.2.

Введите действительную часть числа
5
Введите мнимую часть комплексного числа
-7
Введите действительную часть числа
11
Введите мнимую часть комплексного числа
1.85
5 -7 i
11 +1.85 i
8.60233 ( cos (-0.950547) + i sin (-0.950547))
11.1545 ( cos (0.166623) + i sin (0.166623))
Сумма чисел 16 -5.15 i
Разность чисел -6 -8.85 i
Произведение чисел 67.95 -67.75 i
Частное чисел 0.337961 -0.693203 i
8.60233 ( cos (-0.950547) + i sin (-0.950547))
1900 +47068 i
8.60233 ( cos (-0.950547) + i sin (-0.950547))
0-е значение корня
1.51018 -0.290608 i
1-е значение корня
0.743054 +1.34646 i
2-е значение корня
-1.05094 +1.12277 i
3-е значение корня
-1.39257 -0.652552 i
4-е значение корня
0.190285 -1.52606 i

9.2 Библиотеки для работы с комплексными числами

Работа с комплексными числами в C++ реализована с помощью библиотеки complex. Подключение этой библиотеки даёт возможность применять операции +, _, *, / для работы не только с вещественными, но и с комплексными числами.

Перед подключением библиотеки complex обязательно необходимо подключить библиотеку math.h.

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

complex <тип_переменной> имя_переменной;

Здесь тип_переменной — это любой допустимый в C++ числовой тип данных (int, long int, double, float и т. д.), описывающий действительную и мнимую части комплексного числа. Например,

complex <float > x, y, z [ 5 ], * r;
complex <double> a;
complex <int> a, b, c;

Для организации ввода-вывода комплексных чисел можно использовать библиотеку iostream и стандартные конструкции cin, cout. Например,

#include <iostream>
#include <math.h>
#include <complex>
using namespace std;
int main ( int arg c, char ** argv )
{
	complex <double> b, c; //Описание комплексных чисел.
	cout<<" b = "; cin>>b; //Ввод комплексного числа b.
	cout<<" c = "; cin>>c; //Ввод комплексного числа c.
	cout<<" b / c = "<<b/ c; //Вывод частного комплексных чисел
	return 0;
}

В результате получим:

b=(1.24,-6.12)
c=(9.01,-11.22)
b/c=(0.385567,-0.199105)

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

(действительная_часть, мнимая_часть)

Далее приведён пример присваивания комплексным переменным реальных значений при их описании:

complex <double> z ( 4.0, 1.0 );
complex <int> r ( 4, -7);

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

#include <iostream>
#include <math.h>
#include <complex>
using namespace std;
int main ( int arg c, char ** argv )
{
	double x1, y1;
	x1=-2.3;
	y1 = 8.1;
	complex <double> b ( x1, y1 ); //Формирование комплексного числа b
	//с действительной частью x1 и мнимой y1.
	cout<<" b ^ 2 = "<<b*b; //Вывод квадрата комплексного числа.
	return 0;
}

В табл. 9.1 представлены основные математические функции для работы с комплексными числами.

Таблица 9.1. Основные функции комплексного аргумента
Прототип функцииОписание функции
double abs(complex z)Возвращает модуль комплексного числа .
double arg(complex z)Возвращает значение аргумента комплексного числа
complex conj(complex z)Возвращает число комплексно сопряжённое числу
complex cos(complex z)Возвращает косинус комплексного числа
complex cosh(complex z)Возвращает гиперболический косинус комплексного числа .
complex exp(complex z)Возвращает экспоненту комплексного числа .
double imag(complex z)Возвращает мнимую часть комплексного числа .
complex log(complex z)Возвращает натуральный логарифм комплексного числа
complex log10(complex z)Возвращает десятичный логарифм комплексного числа .
double norm(complex z)Возвращает квадрат модуля комплексного числа .
complex pow(complex x, complex y)Возвращает степень комплексного числа .
complex polar(double mag, double angle)Формирует комплексное число с модулем и аргументом
double real(complex z)Возвращает действительную часть комплексного числа
complex sin(complex z)Возвращает синус комплексного числа
complex sinh(complex z)Возвращает гиперболический синус комплексного числа .
complex sqrt(complex z)Возвращает квадратный корень комплексного числа
complex tan(complex z)Возвращает тангенс комплексного числа
complex tanh(complex z)Возвращает гиперболический тангенс комплексного числа .

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

#include <iostream>
#include <math.h>
#include <complex>
using namespace std;
int main ( )
{
	complex <double> x ( 4, -6);
	complex <double> y (-7, 2 );
	cout<<" x * y = "<<x*y<<endl;
	cout<<" sin ( x ) * cos ( y ) = "<<sin ( x ) * cos ( y )<<endl;
	cout<<" conj ( x ) * ln ( y ) = "<<conj ( x ) * log ( y )<<endl;
	cout<<" sh ( y ) = "<<sinh ( y )<<endl;
	return 0;
}

Результаты работы программы с некоторыми функциями комплексного аргумента:

x*y=(-16,50)
sin(x)*cos(y)=(-747.159,10.2102)
conj(x)*ln(y)=(-9.23917,23.364)
sh(y)=(228.18,498.583)

Задача 9.3. Вычислить .

Если провести аналитические преобразования, то получим следующее:

Проверим эти вычисления с помощью программы на C++. Результаты работы программы подтверждают аналитические вычисления.

#include <iostream>
#include <math.h>
#include <complex>
using namespace std;
int main ( )
{
	complex <double> b ( sqrt ( 3 ),-1), y;
	y=pow ( b, 20 );
	cout<<" y = "<<y<<endl;
	cout<<real ( y ) /pow ( 2, 1 9 )<<" \t ";
	cout<<imag ( y ) /pow ( 2, 1 9 )<<" \n ";
	complex <double> a ( 1, sqrt ( 3 ) ), c (1, -1), z;
	z=pow ( a / c, 40 );
	cout<<" z = "<<z<<endl;
	cout<<real ( z ) /pow ( 2, 19 )<<" \t ";
	cout<<imag ( z ) /pow ( 2, 19 )<<" \n ";
	return 0;
}

Результаты работы программы к задаче 9.3:

b=(1.73205,-1)y=(-524288,908093)
-1 1.73205
z=(-524288,-908093)
-1 -1.73205

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

Задача 9.4. Написать программу умножения матриц комплексных чисел. Матрицы и имеют вид:

Пусть исходные данные хранятся в файле abc.txt. Данные к задаче 9.4, содержимое файла abc.txt:

3 4 5
(1,2) (2,3) (3,1.54) (4,-7.2)
(2,5) (3,7) (4,10) (5,14)
(1.5,3.25) (1.7,-3.94) (6.23,11.17) (-4.12,3.62)

(6.23,-1.97) (0.19,0.22) (0.16,0.28) (3.4,1.95) (2.20,-0.18)
(0.22,0.29) (11,12) (6.72,-1.13) (16,18) (34,66)
(5,1) (1.4,-1.76) (4.5,2.3) (296,700) (4.2,1.03)
(-3.14,-2.61) (1,11) (2,23) (3,-35) (4,47)

Далее приведён текст программы, реализующий алгоритм решения задачи 9.4.

#include <iostream>
#include <fstream>
#include <math.h>
#include <complex>
using namespace std;
int main ( )
{
int i, j, p,N,M,K;
complex <float > **A, **B, **C;
ifstream f;
ofstream g;
f.open ( " abc.txt " );
f>>N>>M>>K;
cout<<" N = "<<N<<" \tM = "<<M<<" \tK = "<<K<<endl;
A=new complex <float > * [N ];
for ( i =0; i<N;A [ i ]=new complex <float > [M], i++);
B=new complex <float > * [M];
for ( i =0; i<M;B [ i ]=new complex <float > [K ], i++);
C=new complex <float > * [N ];
for ( i =0; i<N;C [ i ]=new complex <float > [K ], i++);
for ( i =0; i<N; i++)
	for ( j =0; j<M; f>>A [ i ] [ j ], j++);
cout<<"Матрица A\n ";
for ( i =0; i<N; cout<<endl, i++)
	for ( j =0; j<M; cout<<A [ i ] [ j ]<<" \t ", j++);
for ( i =0; i<M; i++)
	for ( j =0; j<K; f>>B [ i ] [ j ], j++);
cout<<"Матрица B\n ";
for ( i =0; i<M; cout<<endl, i++)
	for ( j =0; j<K; cout<<B [ i ] [ j ]<<" \t ", j++);
for ( i =0; i<N; i++)
	for ( j =0; j<K; j++)
		for (C [ i ] [ j ]=p=0;p<M; p++)
			C [ i ] [ j ]+=A [ i ] [ p ] *B [ p ] [ j ];
f.close ( );
cout<<"Матрица C\n ";
for ( i =0; i<N; cout<<endl, i++)
	for ( j =0; j<K; cout<<C [ i ] [ j ]<<" \t ", j++);
g.open ( " result.t x t " );
g<<"Матрица C=A*B\n ";
for ( i =0; i<N; g<<endl, i++)
	for ( j =0; j<K; g<<C [ i ] [ j ]<<" \t ", j++);
g.close ( );
return 0;
}

Результат умножения матриц из задачи 9.4 (файл result.txt):

Матрица C=A*B
(-8.152,34.598)		(75.8604,91.276)	(199.988,109.93)	(-452.5,2486.99)	(237.974,406.978)
(51.78,26.61)		(-177.52,190.35)	(-290.01,242.21)	(-5391.95,5813.9)	(-986.2,783.76)
(59.6291,78.3851)	(49.9912,-59.0193)	(-82.8542,-50.3838)	(-5763.7,7803.92)	(149.766,-140.709)

Задача 9.5. Заданы матрицы и . Необходимо вычислить матрицу , обратную к матрице , найти определитель матрицы и решить матричное уравнение , где . Матрицы и имеют вид:

Для хранения исходных данных создадим текстовый файл abc2.txt следующего содержания:

3
(1,2) (2,3) (3,1.54)
(2,5) (3,7) (4,10)
(1.5,3.25) (1.7,-9.34) (6.23,11.17)

(1.5,3.25) (1.7,-9.34) (6.23,11.17)
(0.11,8.22) (0.34,-18.21) (1,-7)
(1,5) (7,-13) (12,89)

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

#include <iostream>
#include <fstream>
#include <math.h>
#include <complex>
using namespace std;
//Решение СЛАУ с комплексными коэффициентами
int SLAU( complex <float > ** matrica_a, int n, complex <float > *massiv_b,
complex <float > *x )
{
int i, j, k, r;
complex <float > c,M, s;
float max;
complex <float > **a, *b;
a=new complex <float > *[ n ];
for ( i =0; i<n; i++)
	a [ i ]=new complex <float >[n ];
b=new complex <float > [ n ];
for ( i =0; i<n; i++)
	for ( j =0; j<n; j++)
		a [ i ] [ j ]=matrica_a [ i ] [ j ];
for ( i =0; i<n; i++)
	b [ i ]=massiv_b [ i ];
for ( k=0;k<n; k++)
{
	max= abs ( a [ k ] [ k ] );
	r=k;
	for ( i=k+1; i<n; i++)
		if ( abs ( a [ i ] [ k ] )>max)
		{
			max=abs ( a [ i ] [ k ] );
			r= i;
		}
	for ( j =0; j<n; j++)
	{
		c=a [ k ] [ j ];
		a [ k ] [ j ]=a [ r ] [ j ];
		a [ r ] [ j ]= c;
	}
	c=b [ k ];
	b [ k ]=b [ r ];
	b [ r ]= c;
	for ( i=k+1; i<n; i++)
	{
		for (M=a [ i ] [ k ] / a [ k ] [ k ], j=k; j<n; j++)
			a [ i ] [ j ]_=M_a [ k ] [ j ];
		b [ i ]-=M*b [ k ];
	}
}
if ( abs ( a [ n -1 ] [ n-1 ])==0)
	if ( abs ( b [ n-1 ])==0)
		return -1;
	else return -2;
else
{
	for ( i=n-1; i >=0; i --)
	{
		for ( s =0, j= i +1; j<n; j++)
			s+=a [ i ] [ j ] * x [ j ];
		x [ i ]=( b [ i ]- s ) /a [ i ] [ i ];
	}
return 0;
}
for ( i =0; i<n; i++)
	delete [ ] a [ i ];
delete [ ] a;
delete [ ] b;
}
//Вычисление обратной матрицы с комплексными коэффициентами
int INVERSE( complex <float > **a, int n, complex <float > **y )
{
int i, j, res;
complex <float > *b, *x;
b=new complex <float > [ n ];
x=new complex <float > [ n ];
for ( i =0; i<n; i++)
{
	for ( j =0; j<n; j++)
		if ( j==i )
		b [ j ]= 1;
		else b [ j ]= 0;
			res=SLAU( a, n, b, x );
	if ( res !=0)
		break;
	else
		for ( j =0; j<n; j++)
			y [ j ] [ i ]=x [ j ];
}
delete [ ] x;
delete [ ] b;
if ( res !=0)
	return -1;
else
	return 0;
}
//Вычисление определителя матрицы с комплексными коэффициентами
complex <float > determinant ( complex <float > ** matrica_a, int n )
{
int i, j, k, r;
complex <float > c,M, s, det =1;
complex <float > **a;
float max;
a=new complex <float > * [ n ];
for ( i =0; i<n; i++)
	a [ i ]=new complex <float >[n ];
for ( i =0; i<n; i++)
	for ( j =0; j<n; j++)
		a [ i ] [ j ]=matrica_a [ i ] [ j ];
for ( k=0;k<n; k++)
{
	max=abs ( a [ k ] [ k ] );
	r=k;
	for ( i=k+1; i<n; i++)
		if ( abs ( a [ i ] [ k ] )>max)
		{
			max=abs ( a [ i ] [ k ] );
			r= i;
		}
	if ( r !=k ) det=-det;
		for ( j =0; j<n; j++)
		{
			c=a [ k ] [ j ];
			a [ k ] [ j ]=a [ r ] [ j ];
			a [ r ] [ j ]= c;
		}
	for ( i=k+1; i<n; i++)
		for (M=a [ i ] [ k ] / a [ k ] [ k ], j=k; j<n; j++)
			a [ i ] [ j ]-=M*a [ k ] [ j ];
}
for ( i =0; i<n; i++)
	det*=a [ i ] [ i ];
return det;
for ( i =0; i<n; i++)
	delete [ ] a [ i ];
delete [ ] a;
}
//Умножение матриц с комплексными коэффициентами
void umn ( complex <float > **a, complex <float > **b, complex <float > **c, int
	n, int m, int k )
{
int i, j, p;
for ( i =0; i<n; i++)
	for ( j =0; j<k; j++)
		for ( c [ i ] [ j ]=p=0;p<m; p++)
			c [ i ] [ j ]+=a [ i ] [ p ] * b [ p ] [ j ];
}
int main ( )
{
int i, j,N;
complex <float > **A, **B, **X, **Y;
ifstream f;
ofstream g;
f.open ( " abc2.txt " );
f>>N;
cout<<" N = "<<N<<endl;
A=new complex <float > * [N ];
for ( i =0; i<N; i++)
	A [ i ]=new complex <float > [N ];
B=new complex <float > * [N ];
for ( i =0; i<N; i++)
	B [ i ]=new complex <float > [N ];
X=new complex <float > * [N ];
for ( i =0; i<N; i++)
	X [ i ]=new complex <float > [N ];
Y=new complex <float > * [N ];
for ( i =0; i<N; i++)
	Y [ i ]=new complex <float > [N ];
for ( i =0; i<N; i++)
	for ( j =0; j<N; j++)
		f>>A [ i ] [ j ];
cout<<"Матрица A\n ";
for ( i =0; i<N; cout<<endl, i++)
	for ( j =0; j<N; j++)
		cout<<A [ i ] [ j ]<<" \t ";
for ( i =0; i<N; i++)
	for ( j =0; j<N; j++)
		f>>B [ i ] [ j ];
cout<<"Матрица B\n ";
for ( i =0; i<N; cout<<endl, i++)
	for ( j =0; j<N; j++)
		cout<<B [ i ] [ j ]<<" \t ";
if ( ! INVERSE(A, N, X) )
{
	cout<<"Обратная матрица\n ";
	for ( i =0; i<N; cout<<endl, i++)
		for ( j =0; j<N; j++)
			cout<<X [ i ] [ j ]<<" \t ";
	umn(X, B,Y,N,N,N);
	cout<<" \n Решение матричного уравнения \n ";
	for ( i =0; i<N; cout<<endl, i++)
		for ( j =0; j<N; j++)
			cout<<Y [ i ] [ j ]<<" \t ";
}
else cout<<"Не существует обратной матрицы\n ";
cout<<"Определитель= "<<determinant (A,N);
return 0;
}

Результат работы программы к задаче 9.5:

N=3
Матрица A
(1,2) (2,3) (3,1.54)
(2,5) (3,7) (4,10)
(1.5,3.25) (1.7,-9.34) (6.23,11.17)
Матрица B
(1.5,3.25) (1.7,-9.34) (6.23,11.17)
(0.11,8.22) (0.34,-18.21) (1,-7)
(1,5) (7,-13) (12,89)
Обратная матрица
(-0.495047,-0.748993) (0.325573,0.182901) (-0.0340879,-0.0958618)
(0.125154,0.0765918) (-0.058179,-0.0728342) (0.00208664,0.0685887)
(0.157733,0.322512) (-0.0859214,-0.127174) (0.0143863,-0.000518244)
Определитель=(7.50219,-208.261)
Решение матричного уравнения
(0.669246,-0.302366) (-5.88068,-2.74393) (15.0106,-16.4762)
(0.190248,0.114415) (0.488295,0.448942) (-6.72319,3.21833)
(0.241332,0.347549) (1.02932,0.405788) (-3.37716,5.51956)

9.3 Задачи для самостоятельного решения

9.3.1 Структуры. Операции над комплексными числами

Разработать программу на языке C++ для решения следующей задачи. Даны комплексные числа и . Найти комплесное число по формуле, представленной в табл. 9.2.

Таблица 9.2. Задания для решения задачи о комплексных числах
ВариантФормула для вычислений
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

9.3.2 Работа с библиотекой комплексных чисел

Разработать программу на языке C++ для решения следующей задачи:

  1. Для заданной матрицы комплексных чисел найти .
  2. Для заданных матриц комплексных чисел и найти .
  3. Для заданных матриц комплексных чисел и найти , где .
  4. Для заданной матрицы комплексных чисел найти .
  5. Для заданных матриц комплексных чисел и найти .
  6. Для заданных матриц комплексных чисел и найти , где .
  7. Для заданной матрицы комплексных чисел найти .
  8. Для заданных матриц комплексных чисел и найти .
  9. Для заданных матриц комплексных чисел и найти , где .
  10. Для заданной матрицы комплексных чисел найти .
  11. Для заданных матриц комплексных чисел и найти .
  12. Для заданных матриц комплексных чисел и найти , где .
  13. Для заданной матрицы комплексных чисел найти .
  14. Для заданных матриц комплексных чисел и найти .
  15. Для заданных матриц комплексных чисел и найти , где .
  16. Для заданной матрицы комплексных чисел найти .
  17. Для заданных матриц комплексных чисел и найти .
  18. Для заданных матриц комплексных чисел и найти , где .
  19. Для заданной матрицы комплексных чисел найти .
  20. Для заданных матриц комплексных чисел и найти .
  21. Для заданных матриц комплексных чисел и найти , где .
  22. Для заданной матрицы комплексных чисел найти .
  23. Для заданных матриц комплексных чисел и найти .
  24. Для заданных матриц комплексных чисел и найти , где .
  25. Для заданной матрицы комплексных чисел найти .

Лекция 10. Объектно-ориентированное программирование

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

10.1 Возникновение объектного подхода в программировании

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

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

Процедурный подход к программированию


увеличить изображение

Рис. 10.1.  Процедурный подход к программированию

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

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

В упрощённом виде такая парадигма получила название объектноориентированного программирования (ООП) — подхода, который позволяет использовать в программе объекты и даже поощряет эту практику, но не требует, чтобы программа состояла из одних только объектов.

Классическое определение объекта звучит следующим образом:

Объект — это осязаемая сущность, которая чётко проявляет своё поведение.

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

Объект состоит из следующих трёх частей:

На рис. 10.2 изображены два объекта с именами "Объект 1" и "Объект 2". Первый объект имеет две переменные состояния и три метода, в то время как второй объект обходится одной единственной переменной состояния и двумя методами.

Объектный подход к программированию


увеличить изображение

Рис. 10.2.  Объектный подход к программированию

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

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

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

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

Более того, когда мы рассматриваем сущности реального мира, с которыми должна иметь дело создаваемая программа, мы можем назначить некую сущность на роль объекта или на роль класса объектов, также в зависимости от конкретной задачи. Представим себе, что одна из подзадач программы — систематизировать социальные роли, такие как "домохозяйка", "пенсионер", "студент" (например, для учёта доходов и льгот). Вполне вероятно, что в такой программе "социальная роль" будет объявлена классом, а сущность "студент" будет всего лишь одним из объектов.

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

В реальном мире из родственных по смыслу сущностей часто можно составить иерархию "от общего к частному". Такие отношения в объектно- ориентированном подходе называются наследованием. Из двух классов, находящихся в отношении наследования, более общий класс называется базовым или родительским классом, а класс, представляющий собой более частный случай, называется дочерним или производным классом. Производный класс может заимствовать атрибуты (свойства и методы) базового класса. Это означает, что если в программе используются родственные по смыслу классы, обладающие некоторыми одинаковыми свойствами и методами — лучше определить один базовый класс, находящийся в вершине иерархии, и разместить дублирующиеся свойства и методы в нём. В этом случае производные классы смогут автоматически унаследовать эти атрибуты от базового класса, и поэтому их не придётся описывать снова и снова. Например, если программа оперирует классами "студент", "преподаватель" и "инженер", логично ввести дополнительный базовый класс "человек", переместив в него атрибуты, содержащие имя, адрес, другие личные данные, а также методы, манипулирующие этими данными.

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

Инкапсуляция, наследование и полиморфизм являются тремя основополагающими принципами объектно-ориентированного программирования и в том или ином виде реализуются во всех объектно-ориентированных языках. В следующих разделах мы увидим, как конкретно эти принципы применены в C++.

10.2 Классы и объекты в C++

Хотя C++ и не первая попытка создать объектно-ориентированную версию языка С, среди попыток такого рода он оказался наиболее успешным. Очевидно, одна из причин успешности — то, каким образом объектная парадигма была встроена в синтаксис языка.

Ранние версии C++ были созданы в начале 1980-х Бьёрном Страуструпом для собственных нужд (в качестве универсального языка программирования, удобного для компьютерного моделирования). В создаваемый язык были заложены следующие основные принципы:

Эти принципы заслужили высокую оценку программистов-практиков. В результате на текущий момент C++ является одним из наиболее распространённых языков программирования — как системного, так и прикладного.

10.2.1 Реализация ООП в C++. Классы и структуры

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

class имя_класса
{
	закрытые члены класса
	public :
	открытые члены класса
};

Как и при объявлении структуры, имя_класса становится новым именем типа данных, которое можно использовать для объявления переменных (объектов класса) [5, 6]. Членами класса будут переменные и функции, объявленные внутри класса. Функции-члены класса называют методами этого класса, а переменныечлены класса называют свойствами класса.

В C++ понятия ООП используются следующим образом 5, 6]:

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

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

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

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

class spatial_vector
{
public :
	double abs ( );
private :
	double x, y, z;
};
struct spatial_vector
{
	double abs ( );
private :
	double x, y, z;
};

Добавив в структуру или в класс какой-нибудь метод, программист может потом вызвать этот метод для конкретного объекта. Обращение к содержимому объекта выполняется так же, как к содержимому структурной переменной: с использованием операции "." (либо операции "->", если нужно обратиться по указателю на объект).

main ( )
{
	spatial_vector a,b;
	double d;
	.......
	d = a.abs ( );
}

Очевидно, что функция abs(), объявленная в классе spatial_vector, возвращает абсолютное значение вектора. Однако для того, чтобы программа скомпилировалась, после объявления функцию abs() нужно ещё определить (т. е. написать тело этой функции). Определение метода выполняется так же, как обычной функции, только в имени метода нужно указать, что он принадлежит конкретному классу. Для этого используется оператор расширения области видимости "::". Имя класса записывается перед именем функции, отделённое двойным двоеточием. Например, в следующем примере мы объявим всё тот же класс spatial_vector с двумя методами (установить значения координат вектора и посчитать его модуль) и опишем эти методы:

#include <iostream>
#include <math.h>
using namespace std;
class spatial_vector
{
	double x, y, z;
public :
	void set ( double a,double b,double c );
	double abs ( );
};
void spatial_vector::set ( double a,double b,double c )
{
	x=a;y=b;z=c;
}
double spatial_vector::abs ( )
{
	return sqrt ( x*x + y*y + z*z );
}
main ( )
{
	spatial_vector a;
	a.set ( 1, 2, 3 );
	cout << a.abs ( ) << endl;
}

10.2.2 Создание и удаление объекта: конструкторы и деструкторы

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

class spatial_vector
{
	double x, y, z;
public :
	double get_x ( );
	void set_x ( double x );
... .
double spatial_vector::get_x ( ) { return x; }
... .
}

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

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

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

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

У описания конструкторов в C++ есть следующие особенности:

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

В C++ деструкторы имеют имена, состоящие из имени класса с префиксом- тильдой: "˜имя_класса". Как и конструктор, деструктор не возвращает никакое значение, но в отличие от конструктора он не может быть вызван явно. Причины такого ограничения очевидны: код, предположительно освобождающий динамическую память, будет обязательно вызван при выходе из области видимости. Его явный вызов привёл бы к тому, что память уже освободилась заранее, а при уничтожении объекта программа попыталась бы сделать это снова и сгенерировала бы ошибку.

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

#include <iostream>
#include <math.h>
using namespace std;
class spatial_vector
{
	double x, y, z;
public :
	spatial_vector ( );
	˜ spatial_vector ( ) { cout << "Работа деструктора\n "; }
	double abs ( ) { return sqrt ( x*x + y*y + z*z ); }
};
spatial_vector::spatial_vector ( )
{
	//конструктор класса vector
	x=y=z =0;
	cout << "Работа конструктора\n ";
}
main ( )
{
	spatial_vector a;//создаётся объект a c нулевыми значениями
	cout << a.abs ( ) << endl;
}

Будучи выполнена, программа выводит следующие сообщения:

Работа конструктора
0
Работа деструктора

Обратите внимание на то, что тела функции abs() и деструктора были описаны непосредственно при объявлении, внутри описания класса. Такой подход обычно применяют для очень простых и коротких методов с тривиальным содержимым. В соответствии с традицией, унаследованной от языка С, описания классов в больших программах на C++ обычно выносятся в заголовочные файлы, в отличие от описания методов. Однако помещение описания простых методов внутрь описания класса имеет дополнительный практический смысл. Компилятор пытается сделать код таких методов встроенным (англ. inline). Это означает, что при обращении к методу вызов соответствующей функции будет заменён непосредственно на её код. Благодаря такому трюку массовое обращение к свойствам класса через его методы (геттеры или сеттеры) не обязательно снижает производительность в сравнении с тем, если бы свойства находились в открытой секции.

10.2.3 Передача параметров в конструкторы

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

#include <iostream>
#include <math.h>
using namespace std;
class spatial_vector
{
	double x, y, z;
public :
	spatial_vector ( double x, double y, double z );
	˜ spatial_vector ( ) { cout << "Работа деструктора\n "; }
	double abs ( ) { return sqrt ( x*x + y*y + z*z ); }
};
spatial_vector::spatial_vector ( double x1, double y1, double z1 )
{
	x = x1;
	y = y1;
	z = z1;
	cout << "Работа конструктора\n ";
}
main ( )
{
	spatial_vector a(3, 4, 0 );
}

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

10.2.4 Указатель this

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

Вместо бессмысленного расхода памяти на идентичные дубликаты методов, мы обращаемся к коду метода, передавая ему контекст вызова — указание на то, для какого объекта этот метод в данный момент вызван. Контекст передаётся с помощью дополнительного скрытого параметра, который функция-член класса получает в момент вызова: это указатель на переменную-экземпляр класса, для которого функция вызвана. Этот указатель имеет имя this. Если в теле метода используется переменная, которая не описана в нём и не является глобальной, автоматически считается, что она является членом класса и принадлежит переменной this.

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

... .
class point
{
	int x, y;
public :
	point ( int x, int y )
	{
		this->x=x; this->y=y;
	}
... .
}

10.2.5 Дружественные функции

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

Чтобы класс мог предоставлять внешним функциям доступ к своей закрытой части, используется механизм объявления дружественных функций (friend) класса. Внутрь описания класса помещается прототип функции, перед которым ставится ключевое слово friend. В качестве примера, рассмотрим всё тот же класс point. Объявим дружественную этому классу функцию find_point, выполняющую поиск точки с заданными координатами в массиве объектов. Пусть функция принимает три аргумента: указатели на первый и последний элементы массива, среди которых нужно выполнять поиск, а также собственно аргумент поиска, т. е. точку с искомыми координатами.

class point
{
private :
	int x, y;
	..... .
	friend void find_point ( point* first, point * last, point arg );
};
void find_point ( point * first, point * last, point arg )
{
	for ( point *p= first; p<=last; p++)
		if ( ( p->x == arg.x ) && ( p->y == arg.y ) )
			cout << "Точка с координатами " << p->x << ", " << p->y << " найдена\n ";
}

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

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

Функция-элемент одного класса может быть дружественной другому классу. Например:

class x
{
	..... .
public :
	void f ( ) { };
};
class y
{
	..... .
	friend void x::f ( );
};

Если все функции одного класса дружественны другому классу, можно использовать сокращённую запись:

class y
{
	//.. .
	friend class x;
};

10.2.6 Статические свойства и методы класса

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

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

Чтобы объявить статический элемент класса, перед ним необходимо указать ключевое слово static. Для примера добавим в класс point статическое свойство count — счётчик, указывающий, сколько экземпляров данного класса существует в памяти в настоящий момент. Очевидно, что управлять содержимым счётчика будут конструктор и деструктор класса.

#include<iostream>
using namespace std;
class point
{
	int x, y;
	//.. .
	static int count;
public :
	point ( ) { cout << "Создаётся точка с номером " << ++count << endl; }
	˜ point ( ) { cout << "Разрушается точка с номером " << count-- << endl; }
};
int point::count;
main ( ) {
	point a,b,c;
}

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

Создаётся точка с номером 1
Создаётся точка с номером 2
Создаётся точка с номером 3
Разрушается точка с номером 3
Разрушается точка с номером 2
Разрушается точка с номером 1

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

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

#include<iostream>
#include<time .h>
using namespace std;
class alarm
{
	time_t alarm_t;
public :
	static void current_time ( );
	//....
};
void alarm::current_time ( )
{
	time_t t = time (NULL); //получаем текущее время в нотации Unix, в виде числа секунд,
		//прошедших с 1 января 1970 г.
	struct tm tm = * localtime (& t ); //переводим в местное текущее время
	cout << tm.tm_hour << " : " << tm.tm_min << " : " << tm.tm_sec << endl;
}
main ( )
{
	alarm::current_time ( );
}

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

10.2.7 Перегрузка операторов

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

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

В C++ это явление довели до логического завершения, и теперь многие встроенные операторы можно перегрузить для работы с новыми типами данных. Чтобы перегрузить оператор, программист объявляет новую функцию, имя которой состоит из ключевого слова operator и знака операции. Например, перегрузим оператор + для сложения двух объектов класса spatial_vector. Объявление функции будет выглядеть следующим образом:

spatial_vector operator+ ( spatial_vector a,spatial_vector b )
{
......... .
}

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

#include <iostream>
#include <math.h>
using namespace std;
class spatial_vector
{
	double x, y, z;
public :
	spatial_vector ( double x, double y, double z ) { this->x=x; this->y=y; this->z=z; }
	double get_x ( ) { return x; }
	double get_y ( ) { return y; }
	double get_z ( ) { return z; }
	void set_x ( double x ) { this->x=x; }
	void set_y ( double y ) { this->y=y; }
	void set_z ( double z ) { this->z=z; }
	void info ( ) { cout << "Координаты вектора: "<<x<<", "<<y<<", "<<z<<endl; }
};
spatial_vector operator+ ( spatial_vector a,spatial_vector b )
{
	spatial_vector c ( 0, 0, 0 );
	c.set_x ( a.get_x ( ) + b.get_x ( ) );
	c.set_y ( a.get_y ( ) + b.get_y ( ) );
	c.set_z ( a.get_z ( ) + b.get_z ( ) );
	return c;
}
main ( )
{
	spatial_vector a(1, 2, 3 ), b(10, 20, 30 ), c ( 0, 0, 0 );
	c=a+b;
	c.info ( );
}

10.2.8 Перегрузка членов класса

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

class spatial_vector
{
	double x, y, z;
public :
	spatial_vector ( double x, double y, double z );
	spatial_vector ( );
... .
};

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

spatial_vector ( double x=0, double y=0, double z=0);

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

Операторы тоже можно перегружать как члены класса, но с некоторыми интересными особенностями. Если мы вызываем в программе метод класса — его вызов будет указан после имени конкретного объекта. Как читатель безусловно помнит, при этом методу передаётся скрытый указатель на объект. Если перегруженный оператор объявлен как член класса, то компилятор, встретив его вызов, должен определить, для какого объекта вызвана перегружающая оператор функция, и тоже передать ей скрытый указатель на объект. Таким объектом всегда является левый операнд. По этой причине в объявлении перегруженного оператора внутри класса нет необходимости упоминать собственный объект — ведь он передаётся скрытым указателем this. Поэтому описание бинарного оператора, перегруженного как член класса, имеет всего один операнд (правый), а описание унарного оператора не имеет ни одного операнда.

Следует отметить, что нельзя объявить оператор как статический метод (поскольку статическим методам указатель this при вызове не передаётся) или использовать с оператором аргументы по умолчанию

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

Механизм дружественных функций часто используется при перегрузке операторов для работы с объектами, когда по какой-то причине перегруженный оператор невозможно или нецелесообразно объявлять членом класса. Для сравнения изменим фрагмент примера из п. 10.2.7, переопределив оператор "+" как функцию-член класса (слева) и как дружественную функцию (справа):

class spatial_vector
{
.....
spatial_vector operator+(
	spatial_vector b);
};
spatial_vector spatial_vector : :
	operator+( spatial_vector b )
{
	spatial_vector c;
	c.x = x + b.x;
	c.y = y + b.y;
	c.z = z + b.z;
	return c;
}
class spatial_vector
{
.....
friend spatial_vector operator+(
	spatial_vector a,spatial_vector b);
};
spatial_vector operator+(
	spatial_vector a,spatial_vector b )
{
	spatial_vectorc;
	c.x = a.x + b.x;
	c.y = a.y + b.y;
	c.z = a.z + b.z;
	return c;
}

Классический пример перегрузки оператора как дружественной функции — средства стандартного ввода-вывода в C++. Как известно, операции << и >> выполняют ввод и вывод, если левым аргументом указан один из стандартных объектов ввода-вывода. Предопределённые объекты cin (клавиатура) и cout (дисплей) — экземпляры классов istream и ostream. Этим способом можно вывести любой базовый тип данных C++. Однако, на самом деле при выводе вызывается функция, перегружающая оператор. В частности, для объекта cout будет вызвана функция, имеющая приблизительно следующий вид:

ostream operator<<(ostream, int )

В результате, выражение

cout << "Значение переменной i равно " << i << " \n ";

благодаря такой подстановке будет заменено на

operator<<(operator<<(operator<<(cout, "Значение переменной i равно " ), i ), " \n " );

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

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

В общем виде операция вывода имеет следующую форму

ostream& operator << ( ostream& stream, имя_класса& obj )
{
	stream <<...//вывод элементов объекта obj в поток stream
	return stream;
}

Аналогичным образом может быть определена функция ввода:

istream& operator >> ( istream& stream, имя_класса& obj )
{
	stream >>...//ввод элементов объекта obj из потока stream
	return stream;
}

Знак "&&quot; в списке формальных параметров означает, что компилятор обеспечивает скрытую передачу параметра не по значению, а по ссылке (передача объектов по ссылке детально рассмотрена далее, в п. 10.3.2. Первый аргумент функций ввода и вывода определён как ссылка на поток, второй аргумент — ссылка на объект, выводящий или получающий информацию, а возвращаемое значение — тот же самый объект потока, который был передан в качестве первого аргумента.

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

#include <iostream>
using namespace std;
class spatial_vector
{
	double x, y, z;
public :
	spatial_vector ( ) { x=y=z =0; }
	friend ostream& operator<< ( ostream& stream, spatial_vector& b);
	friend istream& operator>> ( istream& stream, spatial_vector& b);
};
ostream& operator<< ( ostream& stream, spatial_vector& b )
{
	stream << " x = " << b.x << "; y = " << b.y << "; z = " << b.z << endl;
	return stream;
}
istream& operator>> ( istream& stream, spatial_vector& b )
{
	stream >> b.x >> b.y >> b.z;
	return stream;
}
main ( )
{
	spatial_vector a;
	cin >> a;
	cout << "Был введён вектор: " << a << endl;
}

10.2.9 Перегрузка постфиксных операторов

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

Объявление члена класса с именем operator++ без аргументов перегружает префиксный оператор инкремента. Чтобы перегрузить функцию-член класса как постфиксный оператор, его нужно объявить с одним аргументом типа int. Этот аргумент не несёт никакой полезной нагрузки и нужен только, чтобы можно было различить префиксные и постфиксные операторы. При выполнении этот аргумент будет иметь нулевое значение. Следующий пример показывает разницу в описаниях, и дополнительно выводит в консоль информацию о том, префиксный или постфиксный оператор был использован. В примере использован класс integer, являющийся обёрткой над переменной типа int, т. е. просто хранящий целое число:

#include <iostream>
using namespace std;
classin teger
{
	int value;
public :
	integer ( ) { value = 0; }
	integer& operator++(); //префиксный оператор
	integer& operator++(int ); //постфиксный оператор
};
integer& integer::operator++()
{
	value +=1;
	cout << "Использован префиксный оператор\n ";
	return *this;
}
integer& integer::operator++(int )
{
	value +=1;
	cout << "Использован постфиксный оператор\n ";
	return *this;
}
main ( )
{
	integer i;
	i ++; //используется постфиксный оператор
	++i; //используется префиксный оператор
}

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

10.3 Создание и удаление объектов

10.3.1 Присваивание объектов, передача в функцию и возвращение объекта

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

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

Рассмотрим в качестве примера класс matrix, хранящий в себе прямоугольную матрицу из элементов типа double. Размерность матрицы будет передаваться конструктору класса, после чего будет выполняться динамическое выделение памяти под нужное количество элементов. В классе будут также предусмотрены методы get_val() чтобы получить элемент матрицы с индексами (i,j) и set_val() чтобы установить в заданный элемент новое значение.

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

#include <iostream>
using namespace std;
class matrix
{
	double *m; //элементы матрицы
	size_t width, height; //число строк и столбцов в матрице
public :
	matrix ( size_t w, size_t h );
	double get_val ( size_t i, size_t j );
	voidset_val ( size_t i, size_t j, double val );
	˜matrix ( );
};
matrix::matrix ( size_t w, size_t h )
{
	m = new double [w*h ];
	width = w;
	height = h;
}
matrix::˜ matrix ( )
{
	delete [ ] m;
}
double matrix::get_val ( size_t i, size_t j )
{
	return m[ i * width+j ]; //получить значение элемента матрицы в позиции [i,j]
}
void matrix : :set_val ( size_t i, size_t j, double val )
{
	//устанавливаем значение элемента матрицы в позиции [i,j]
	//если координаты не превышают размер матрицы
	if ( ( i<width )&&(j<height ) ) m[ i * width+j ]= val;
}
main ( )
{
	matrix a(2, 2 ); //объявляем матрицу размерности 2 х 2
	a.set_val ( 0, 0, 100 ); //устанавливаем a[0,0] = 100
	matrix b=a;//присваиваем матрицу
	b.set_val ( 0, 0, 200 ); //устанавливаем b[0,0] = 200
	cout << " a[0, 0 ] = " << a.get_val ( 0, 0 ) << "; " << " b[ 0, 0 ] = " << a.get_val
		( 0, 0 ) << endl;
}

При запуске программа выдаёт сообщение "a[0,0] = 200; b[0,0] = 200" вместо ожидаемого "a[0,0]=100", после чего и вовсе аварийно завершается с сообщением о попытке дважды освободить память. На самом деле это происходит по вполне очевидной причине. При побитовом копировании скопировался адрес указателя m, а не содержимое блока памяти, динамически выделенного по этому адресу. В результате оба объекта получают указатель на одну и ту же последовательность вещественных чисел.

Аналогично присваиванию, объекты можно передавать в функции в качестве аргументов, в точности так, как передаются данные других типов. Однако следует помнить, что в C++ по-умолчанию параметры передаются по значению. Это означает, что внутри функции (а точнее, в стеке) создаётся копия объектааргумента, и эта копия, а не сам объект, будет далее использоваться функцией. Благодаря этому функции могут произвольно изменять переданные значения, не влияя на оригинал.

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

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

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

Частично проблема может быть решена перегрузкой оператора присваивания для данного класса. Кроме того, для объектов, которым противопоказано побитовое копирование, рекомендуется создавать особую разновидность конструктора — т. н. конструктор копирования (в некоторых источниках также можно встретить название "конструктор копии"). Конструктор копирования выполняет именно то действие, которое заложено в его названии: позволяет программисту лично проконтролировать процесс создания копии.

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

имя_класса ( const имя_класса & obj )
{
	... //тело конструктора
}

Читатель должен помнить, что в таком описании &obj — это ссылка на объект, известная ещё как скрытый указатель.

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

point p1, p2; //объявляем два объекта класса point
point p3 = p2; //используем конструктор копирования
p1 = p2; //используем оператор присваивания

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

Если оператор присваивания для класса не был определён, то в случае необходимости (если для объектов этого класса в тексте программы выполняется присваивание) компилятор автоматически генерирует оператор присваивания по умолчанию, выполняющий то самое побитовое копирование объекта.

10.3.2 Подробнее об указателях и ссылках на объекты

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

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

По этой причине в C++ был введён специальный тип данных — ссылка или скрытый указатель. На понятийном уровне ссылку можно воспринимать как другое имя (псевдоним) переменной. Фактически же это указатель на переменную, который выглядит так, как будто к переменной обращаются по значению: программист объявляет такую ссылку, присваивает ей какую-либо переменную и далее пользуется ссылкой как ещё одной переменной. Компилятор же сам автоматически подставляет ко всем обращениям к ссылке операции адресации и разадресации.

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

Напомним: ссылка объявляется так же как указатель, только с использованием знака "&quot; вместо "звёздочки". Сравним, как выглядит код при передаче аргумента по указателю и по ссылке, на примере функции zero(), устанавливающей в ноль координаты переданного ей объекта класса point:

//использование указателей
void zero ( point *p )
{
	p->set ( 0, 0 );
//мы использовали "->"
}
main ( )
{
	point a(3, 4 );
	zero (&a);
}
//использование ссылки
void zero ( point &p )
{
	p.set ( 0, 0 );
//мы использовали " ."
}
main ( )
{
	point a(3, 4 );
	zero ( a );
}

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

Часто ссылки применяют в сочетании с указателем this. Рассмотрим в качестве примера переопределение оператора присваивания для класса point:

point& point::operator= ( point& p )
{
	x = p.x;
	y = p.y;
	return *this;
}

В объявлении функции мы указали ссылочный тип в качестве как аргумента, так и возвращаемого значения. Оператор присваивания должен возвращать результат операции, чтобы стало возможным каскадное присваивание наподобие a=b=c=0. В качестве возвращаемого значения мы указываем разадресованный указатель this, однако возвращён в качестве результата будет тот объект, который вызывал операцию "=", а не его копия.

Приведём модифицированный вариант класса matrix, имеющий как конструктор копирования, так и оператор присваивания, и выдающий на экран правильный результат.

#include <iostream>
using namespace std;
class matrix
{
	double *m; //элементы матрицы
	size_t width, height; //число строк и столбцов в матрице
public :
	matrix ( size_t w, size_t h );
	matrix ( const matrix& m1.; //конструктор копирования
	matrix& operator=(matrix & m1.; //оператор присваивания
	double get_val ( size_t i, size_t j );
	voidset_val ( size_t i, size_t j, double val );
	˜matrix ( );
};
matrix::matrix ( size_t w, size_t h )
{
	m = new double [w*h ];
	width = w;
	height = h;
}
matrix::matrix ( const matrix& m1)
{
	//устанавливаем размер матрицы и выделяем под неё память:
	width = m1.width;
	height = m1.eight;
	int size=width*height;
	m = new double [ size ];
	//копируем элементы матрицы:
	for ( int i =0; i < size; i++)
	m[ i ]=m1.m[ i ];
}
matrix& matrix::operator=(matrix& m1)
{
	int size=m1.width_m1.height;
	if ( size > width* height )
		//защищаемся от переполнения буфера
		size=width_ height;
	m = new double [ size ];
	//копируем элементы матрицы:
	for ( int i =0; i < size; i++)
		m[ i ]=m1.m[ i ];
	return * this;
}
matrix::˜ matrix ( )
{
	delete [ ] m;
}
double matrix::get_val ( size_t i, size_t j )
{
	//получить значение элемента матрицы в позиции [i,j]
	return m[ i * width+j ];
}
void matrix : :set_val ( size_t i, size_t j, double val )
{
	//устанавливаем значение элемента матрицы в позиции [i,j]...
	//...если координаты не превышают размер матрицы
	if ( ( i<width )&&(j<height ) ) m[ i * width+j ]= val;
}
main ( )
{
	matrix a(2, 2 ); //объявляем матрицу размерности 2 х 2
	a.set_val ( 0, 0, 1 0 0 ); //устанавливаем a[0,0] = 100
	matrix b=a;//присваиваем матрицу
	b.set_val ( 0, 0, 2 0 0 ); //устанавливаем b[0,0] = 200
	cout << " a[0, 0 ] = " << a.get_val ( 0, 0 ) << "; " << " b[ 0, 0 ] = " << a.get_val
	( 0, 0 ) << endl;
}

Внимательный читатель может заметить в коде примера необычную особенность. И конструктор копирования, и оператор присваивания получают доступ к закрытой части переданного объекта m1. На самом деле это вполне естественно. Вспомним: переменные, объявленные в закрытой секции класса, доступны только для методов этого же класса (а не "этого же объекта"). Иными словами, объекты одного класса могут получать доступ к закрытым членам друг друга, хотя используется это не так часто.

10.3.3 Пример: класс spatial_vector в сборе

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

#include <iostream>
#include <math.h>
using namespace std;
class spatial_vector
{
	double x, y, z;
public :
	spatial_vector ( double x=0, double y=0, double z=0);
	double abs ( ) { return sqrt ( x*x + y*y + z * z ); }
	double get_x ( ) { return x; }
	double get_y ( ) { return y; }
	double get_z ( ) { return z; }
	void set_x ( double x ) { this->x=x; }
	void set_y ( double y ) { this->y=y; }
	void s et_ z ( double z ) { this->z=z; }
	void info ( );
	spatial_vector& operator++(); //префиксная форма
	spatial_vector& operator--();
	spatial_vector operator++(int ); //постфиксная форма
	spatial_vector operator--(int );
	friend spatial_vector operator+( spatial_vector a,const spatial_vector& b);
	friend spatial_vector operator-( spatial_vector a,const spatial_vector& b);
	friend ostream& operator<<(ostream& stream, const spatial_vector& b);
	friend istream& operator>>( istream& stream, spatial_vector& b);
};
spatial_vector::spatial_vector ( double x1, double y1, double z1 )
{
	x = x1;
	y = y1;
	z = z1;
}
void spatial_vector::info ( )
{
	cout << "Координаты вектора: x = " << x << "; y = " << y << "; z = " << z << endl;
	cout << "Модуль вектора равен " << abs ( ) << endl;
}
spatial_vector& spatial_vector::operator++()
{
	x++; y++; z++;
	return * this;
}
spatial_vector& spatial_vector::operator--()
{
	x--; y--; z--;
	return * this;
}
spatial_vector spatial_vector::operator++(int )
{
	spatial_vector temp=*this;
	++(*this );
	return temp;
}
spatial_vector spatial_vector::operator--(int )
{
	spatial_vector temp=*this;
	--(*this );
	return temp;
}
spatial_vector operator+ ( spatial_vector a,const spatial_vector& b )
{
	//передаём первый аргумент по значению,
	//поэтому можем изменять его, не влияя на исходный объект:
	a.x += b.x;
	a.y += b.y;
	a.z += b.z;
	//возвращаем изменённую копию первого аргумента:
	return a;
}
spatial_vector operator_( spatial_vector a,const spatial_vector& b )
{
	a.x -= b.x;
	a.y -= b.y;
	a.z -= b.z;
	return a;
}
ostream& operator<<(ostream& stream, const spatial_vector& b )
{
	stream << " x = " << b.x << "; y = " << b.y << "; z = " << b.z << endl;
	return stream;
}
istream& operator>>( istream& stream, spatial_vector& b )
{
	stream >> b.x >> b.y >> b.z;
	return stream;
}
main ( )
{
	spatial_vector a,b(1, 2, 3 );
	cout << " \n1. Заполнение вектора через стандартный ввод\n ";
	cout << "Введите координаты вектора: ";
	cin >> a;
	a.info ( );
	cout << " \n2. Вычитание векторов\n ";
	spatial_vector c = a-b;
	cout << "Координаты вектора с=a-b(1,2,3): " << c;
	cout << " \n3. Изменение координаты вектора с помощью геттеров и сеттеров\n ";
	c.set_x ( c.get_x ( ) +1);
	cout << "После инкремента координаты x, координаты вектора c: " << c;
	cout << " \n4. Инкремент:\nвывод с++: " << c++;
	cout << "Вывод ++с: " << ++c;
}

Функция main() просит пользователя ввести с клавиатуры три координаты вектора, а затем выполняет несколько тестов, демонстрирующих работу методов класса. Например, при вводе значений "1 2 3" выводится следующий результат:

1. Заполнение вектора через стандартный ввод
Введите координаты вектора: 1 2 3
Координаты вектора: x=1; y=2; z=3
Модуль вектора равен 3.74166

2. Вычитание векторов
Координаты вектора с=a-b(1,2,3): x=0; y=0; z=0

3. Изменение координаты вектора с помощью геттеров и сеттеров
После инкремента координаты x, координаты вектора c: x=1; y=0; z=0

4. Инкремент:
вывод с++: x=1; y=0; z=0
Вывод ++с: x=3; y=2; z=2

Как можно заметить, в программе не перегружен оператор присваивания, а в классе spatial_vector не задан конструктор копирования. В данном случае класс не работает с динамической памятью и не нуждается в какой-то особой предварительной инициализации и деинициализации, поэтому выполняемое по умолчанию побитовое копирование объектов оказывается полностью приемлемым. Более того, передача параметра по значению активно используется в перегруженных операторах сложения и вычитания, а также в постфиксной форме инкремента и декремента. В первых двух случаях первый параметр (левый операнд) передаётся по значению, чтобы можно было изменить его и вернуть, не затронув исходный объект. В перегруженных постфиксных операторах используется возврат исходной побитовой копии объекта, снятой до того, как оригинальный объект был изменён.

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

10.4 Наследование

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

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

В C++ новый класс строится на базе уже существующего с помощью конструкции следующего вида:

class parent {.....};
class child : [ модификатор наследования ] parent {.....};

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

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

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

Модификатор private описывает закрытые члены класса, доступ к которым имеют только методы-члены этого класса. Модификатор public предназначен для описания общедоступных элементов, доступ к которым возможен из любого места в программе, где доступен объект данного класса. Модификатор protected используется тогда, когда необходимо, чтобы некоторые члены базового класса оставались закрытыми, но были бы доступны из класса-потомка.

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

Таблица 10.1. Разница между модификаторами наследования и доступа
Модификаторы наследования:
  • public
  • protected
  • private
Модификаторы доступа:
  • public
  • protected
  • private
class point {...};
class vertex : public point
{...};
class point
{
public :
int color;
...
};

То, как изменяется доступ к элементам базового класса из методов производного класса в зависимости от значения модификаторов наследования, показано в табл. 10.2.

Таблица 10.2. Сочетание модификаторов наследования и доступа
Модификатор доступа:Модификатор наследования:
publicprotectedprivate
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privateнет доступанет доступанет доступа

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

10.4.1 Конструкторы и деструкторы при наследовании

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

#include<iostream>
using namespace std;
class parent
{
public :
	parent	( ) { cout << "Работа конструктора базового класса\n "; }
	˜parent ( ) { cout << "Работа деструктора базового класса\n "; }
};
class child : public parent
{
public :
	child ( ) { cout<<"Работа конструктора производного класса\n "; }
	˜ child ( ) { cout<<"Работа деструктора производного класса\n "; }
};
main ( )
{
	child c1;
}

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

Работа конструктора базового класса
Работа конструктора производного класса
Работа деструктора производного класса
Работа деструктора базового класса

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

конструктор_производного_класса (список формальных параметров)
: конструктор_базового_класса (список фактических параметров)
{
	... //тело конструктора производного класса
}

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

#include<iostream>
using namespace std;
class point
{
protected :
	int x, y, color;
public :
	point ( int p1, int p2, int c );
};
point::point ( int p1, int p2, int c )
{
	x=p1;
	y=p2;
	color=c;
}
class vertex : public point
{
	int z;
public :
	vertex ( int p1, int p2, int p3, int c );
};
vertex::vertex ( int p1, int p2, int p3, int c ) : point ( p1, p2, c )
{
	z=p3;
}
main ( )
{
	vertex c1 ( 2, 3, 4, 0 );
}

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

vertex::vertex ( int p1, int p2, int p3 )
: point ( p1, p2 ), z ( p3 )
{
	// z=p3;
}

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

10.4.2 Раннее и позднее связывание

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

Методы, которые вызываются так, являются статическими — в том смысле, что компилятор разбирает ссылки на них во время компиляции. Этот подход экономит ресурсы в момент выполнения программы, однако иногда приводит к нежелательным результатам. Рассмотрим для примера иерархию из двух классов: класса vector, представляющего собой двумерный вектор, и производный от него класс spatial_vector, уже знакомый нам по прежним примерам. Нам будут нужны два метода у каждого из классов: метод info(), выводящий текстовое сообщение и сообщающий, чему равен модуль вектора, и метод abs(), собственно вычисляющий значение модуля. Наследование одного класса от другого в данном случае представляется вполне логичным: в производном классе достаточно будет добавить ещё одну переменную, модифицировать конструктор и функцию вычисления модуля.

#include <iostream>
#include <math.h>
using namespace std;
class vector
{
protected :
	double x, y;
public :
	vector ( double x, double y ) { this->x=x; this->y=y; }
	double abs ( ) { return sqrt ( x*x + y*y ); }
	void info ( ) { cout << "Модуль вектора равен " << abs ( ) << endl; }
};
class spatial_vector : public vector
{
	protected :
	double z;
public :
	spatial_vector ( double x, double y, double z );
	double abs ( ) { return sqrt ( x*x + y*y + z*z ); }
};
spatial_vector::spatial_vector ( double x, double y, double z ) : vector ( x, y )
{
	this->z=z;
}
main ( )
{
	cout << "Создаём вектор на плоскости с координатами 1,2\n ";
	vectora(1, 2 );
	a.info ( );
	cout << "Создаём пространственный вектор с координатами 1,2,3\n ";
	spatial_vector b(1, 2, 3 );
	b.info ( );
}

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

Создаём вектор на плоскости с координатами 1,2
Модуль вектора равен 2.23607
Создаём пространственный вектор с координатами 1,2,3
Модуль вектора равен 2.23607

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

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

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

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

Виртуальные методы описываются с помощью ключевого слова virtual в базовом классе. Это означает, что в производном классе этот метод может быть замещён методом, более подходящим для этого производного класса. Объявленный виртуальным в базовом классе, метод остаётся виртуальным для всех производных классов. Если в производном классе виртуальный метод не будет переопределён, то при вызове будет найден метод с таким именем вверх по иерархии классов (т. е. в базовом классе).

#include <iostream>
#include <math.h>
using namespace std;
class vector
{
protected :
	double x, y;
public :
	vector ( double x, double y ) { this->x=x; this->y=y; }
	virtual double abs ( ) { return sqrt ( x*x + y*y ); }
	void info ( ) { cout << "Модуль вектора равен " << abs ( ) << endl; }
};
class spatial_vector : public vector
{
	protected :
	double z;
public :
	spatial_vector ( double x, double y, double z );
	double abs ( ) { return sqrt ( x*x + y*y + z * z ); }
};
spatial_vector::spatial_vector ( double x, double y, double z ) : vector ( x, y )
{
	this->z=z;
}
main ( )
{
	cout << "Создаём вектор на плоскости с координатами 1,2\n ";
	vectora(1, 2 );
	a.info ( );
	cout << "Создаём пространственный вектор с координатами 1,2,3\n ";
	spatial_vector b(1, 2, 3 );
	b.info ( );
}

Будучи выполнен, пример наконец выдаёт ожидаемый ответ:

Создаём вектор на плоскости с координатами 1,2
Модуль вектора равен 2.23607
Создаём пространственный вектор с координатами 1,2,3
Модуль вектора равен 3.74166

10.4.3 Множественное наследование

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

class a{...};
class b{...};
class C : public A, public b{...};

При этом класс C унаследует как содержимое класса А, так и класса B. При вызове конструктора будут сначала вызваны конструкторы базовых классов (в порядке следования). Деструкторы, как всегда, имеют противоположный порядок вызова.

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

10.4.4 Указатели на базовые классы

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

point * p = new vertex();

По указателю на объект базового класса можно вполне корректно вызвать те методы класса-потомка, которые уже существовали в описании базового класса.

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

class parent
{
public :
	void parent_method ( ) {}
};
class child : public parent
{
public :
	void child_method ( ) {}
};
main ( )
{
	parent *p = new child ( );
	p->parent_method ( );
	( ( child *) p )->child_method ( );
}

Типичное использование указателя на базовый класс, которому присвоен адрес объекта производного класса — хранение либо передача нескольких разнотипных объектов, имеющих общий класс-предок. Например, во многих библиотеках виджетов (графических элементов управления) инструментальная панель, которая может содержать в себе кнопки, надписи, выпадающие списки и т. д., является универсальным контейнером, хранящим указатели на объект базового класса (например, класса widget), от которого унаследованы конкретные элементы управления (классы button, text, list и т. д.). Благодаря возможности использовать указатель на базовый класс, панель реализует один единственный набор методов для добавления и удаления разнотипных элементов, а также для обращения к ним.

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

10.4.5 Абстрактные классы

Иногда, когда функция объявляется в базовом классе, она не выполняет никаких значимых действий, поскольку часто базовый класс не определяет законченный тип, а нужен чтобы построить иерархию. Например, метод paint(), объявленный в классе widget и выполняющий отрисовку виджета на экране, должен переопределяться в классах-потомках, с тем, чтобы выводить на экран изображение кнопки в классе button или текстовую надпись в классе text. Изображение же абстрактного виджета тоже абстрактно, и метод в базовом классе не несёт практической нагрузки.

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

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

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

virtual тип имя_функции (список параметров) = 0;

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

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

10.4.6 Пример иерархии классов — библиотека потокового ввода-вывода

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

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

В предыдущих разделах мы касались использования операторов << и >> вместе с объектами cin и cout, а также отметили, что эти объекты на самом деле являются экземплярами классов istream и ostream. Однако прежде чем рассматривать внутреннее устройство этих и других классов, вовлечённых в реализацию ввода-вывода, необходимо разобраться в самой предметной области. Поэтому рассмотрим подробнее, какие стадии включает в себя принятая в C++ кросс-платформенная реализация ввода-вывода.

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

Модель потокового ввода-вывода в С++


увеличить изображение

Рис. 10.3.  Модель потокового ввода-вывода в С++

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

В рамках двухуровневой модели ввода-вывода, принятой в C++, уровень форматирования делает возможным выполнение следующих процедур:

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

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

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

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

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

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

Мы будем рассматривать упрощённое представление для случая, когда символы представлены в программе в однобайтной кодировке с использованием типа char. В реальности библиотека iostream реализует более универсальное представление данных на основе шаблонов, позволяющее не указывать заранее при описании классов тип данных, используемый для хранения символа. Благодаря этому подходу тот же самый код может применяться, например, для многобайтных кодировок, представленных специальным типом wchar_t. Также мы на данном этапе опустим специальные средства обработки ошибок и других исключительных ситуаций, применённые в данной библиотеке. Подробнее о шаблонах и обработке исключительных ситуаций можно будет узнать в следующих разделах; там же будут пояснены опущенные на данном этапе элементы, и в т. ч. то, как на самом деле объявлены типы библиотеки iostream.

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

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

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

Как читатель успел заметить из собственной практики программирования на C++, активнее всего потоки используются для стандартного ввода-вывода (т. е. ввода с клавиатуры и вывода на дисплей). Для обработки стандартного ввода предусмотрен класс istream, а для обработки стандартного вывода — ostream; оба класса наследуются от ios, приобретая благодаря этому всю специфику, связанную с форматированием, и указатель на потоковый буфер. Для взаимодействия с потоковым буфером в классе istream объявлен перегруженный оператор потокового ввода >>, а в классе ostream — перегруженный оператор потокового вывода <<. Для возможности неформатированного ввода и вывода в этих классах объявлен также ряд методов — таких как read() и write(). Наконец, для случаев, когда необходим двунаправленный ввод-вывод (по аналогии с тем, как файл может открываться одновременно для чтения и записи) с помощью множественного наследования от этих двух классов порождён класс iostream, автоматически приобретающий свойства как входных, так и выходных потоков и используемый как базовый для классов, в которых двунаправленный ввод-вывод действительно востребован.

Иерархия классов потокового ввода-вывода


увеличить изображение

Рис. 10.4.  Иерархия классов потокового ввода-вывода

Строковые потоки, осуществляющие ввод-вывод в памяти, представлены классами istringstream и ostringtstream, порождёнными соответственно от istream и ostream, а также универсальным классом двунаправленного ввода- вывода stringtream, порождённым от iostream. Эти классы включают функции (геттеры и сеттеры) для использования строки в качестве буфера.

Файловый ввод-вывод осуществляют классы ifstream и oftstream, порождённые соответственно от istream и ostream, а также универсальный класс fstream, порождённый от iostream. Эти классы содержат методы для открытия и закрытия файлов, аналогичные функциям fopen() и fclose() языка С.

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

Базовый класс streambuffer олицетворяет собой универсальный потоковый буфер. Будучи абстрактным классом, он не содержит в себе специфики конкретных оконечных устройств; однако в нём объявлены две чисто виртуальные функции: overflow() и underflow(), которые должны быть перегружены в производных классах, чтобы выполнять действительную передачу символов между символьным буфером и конкретными оконечными устройствами.

Класс потокового буфера поддерживает две символьные последовательности: область получения (get), представляющую последовательность символов, получаемых из оконечного устройства, и область выдачи (put), т. е. выходную последовательность для записи на устройство. Также в классе предусмотрены функции, извлекающие очередной символ из буфера (sgetc() и т. д.) и помещающие очередной символ в буфер (sputc() и т. д.), которые обычно используются уровнем форматирования. Дополнительно потоковый буфер содержит также объект локали.

Производный от streambuf класс filebuf используется для работы с файлами и содержит для этого ряд функций, таких как open() и close(). Он также наследует объект локали от базового класса для перекодирования между внешней и внутренней кодировками (например, как уже упоминалось, между кодировкой Unicode и внутренним представлением мультиязычных символов значениями типа wchar_t).

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

Взаимодействие между уровнем форматирования и транспортным уровнем осуществляется следующим образом. Класс ios, как мы уже упоминали, содержит в себе указатель на потоковый буфер. В производных от него классах (таких как fstream или stringstream) содержатся указатели на объекты соответствующих классов транспортного уровня (filebuf или stringbuf). Классы транспортного уровня можно также использовать и непосредственно, для неформатированного ввода-вывода — точно так же, как их использует уровень форматирования.

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

class ios {...};
class ostream : public ios {...};
class istream : public ios {...};
class ofstream : public ostream {...};
class ifstream : public istream {...};
class ostringstream : public ostream {...};
class istringstream : public istream {...};
class iostream : public ostream, public istream {...};
class fstream : public iostream {...};
class stringstream : public iostream {...};

10.5 Обработка исключений

10.5.1 Общие понятия

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

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

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

Для решения этих проблем в C++ был включён новый механизм — обработка исключительных ситуаций.

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

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

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

Программист, желающий использовать исключения, должен поместить вызов кода, в котором исключение может возникнуть, в специальный блок try{}. Следом за этим блоком должен следовать блок catch(){}, внутрь которого помещают код, обрабатывающий исключительную ситуацию. Например, если исключение может возникнуть в некой функции f(), для его обработки нужно написать следующую конструкцию:

f ( )
{
	//генерируем исключение, если возникла соответствующая ситуация
	if (.... ) throw индикатор;
.....
}
.....
try
{
	//вызываем код, который может сгенерировать исключение:
	f ( );
}
catch (индикатор)
{
	//обрабатываем исключение
.....
}

Обработчик исключения всегда следует сразу за блоком try{}. В круглых скобках после catch указывается индикатор исключения, которое этот блок обрабатывает. Чтобы сгенерировать исключение, нужно использовать специальную конструкцию throw, после которой указывается индикатор.

В следующем примере, показывающем, как можно применить обработку исключений для организации диалогового режима работы, мы объявим два пустых класса для использования в качестве индикаторов: класс unknown_exception, означающий получение неизвестного ответа от пользователя, и класс abort_exception, сигнализирующий о необходимости немедленного выхода из программы. Сама программа задаёт пользователю вопрос о выполнении последовательно 100 неких абстрактных пронумерованных команд. Диалог реализуется функцией confirm(), спрашивающей у пользователя подтверждение на выполнение команды с заданным номером и анализирующей ответ ("y"— подтверждение, "n" — отказ, "a" — немедленный выход).

#include <iostream>
#include <math.h>
using namespace std;
class unknown_except ion { };
class abort_ exception { };
bool confirm ( int i )
{
	char c;
	cout << "Подтвердите команду " << i << " ( y / n / a/да/нет/выход) : ";
	cin >> c;
	cin.ignore ( ); //очищаем буфер если введены лишние символы
	switch ( c ) {
	case " y " : return true;
	case " n " : return false;
	case " a " : throw abort_exception ( );
	default : throw unknown_exception ( );
	}
}
main ( )
{
	cout << "Демонстрация диалога подтверждения при выполнении"<<" 100 команд\n ";
	for ( int i =1; i <=100; i++) {
	try{
		if ( confirm ( i ) ) cout << "КОМАНДА "<< i <<" ВЫПОЛНЕНА\n ";
		else cout << "КОМАНДА " << i << " ОТМЕНЕНА\n ";
	}
	catch ( unknown_exception ) {
	cout << "Неизвестный ответ пользователя\n ";
	i --; // возвращаемся к предыдущей команде
	}
	catch ( abort_exception ) {
	cout << "Выполняется немедленный выход из программы\n ";
	return 0;
	}
	cout << "Продолжение демонстрации диалога\n ";
	}
}

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

Обратите внимание, что в блоке catch() мы указали в качестве параметра только тип данных — класс-индикатор. Это допустимо с учётом того, что обработчик исключения не собирается извлекать никаких данных из переданного индикатора, да и сами классы-индикаторы, созданные в программе, являются пустыми и используются только для того, чтобы различать исключительные ситуации.

10.5.2 Передача информации в обработчик

Как уже упоминалось, чтобы передать в обработчик дополнительную информацию, нужно принимать в блоке catch() не тип данных, а переменную. Перед выбрасыванием исключения можно её проинициализировать конкретными данными, а потом прочитать эти данные в обработчике.

Приведём в качестве иллюстрации ещё один пример, в котором реализован класс array, предоставляющий пользователю массив с возможностью добавления и удаления элементов в стиле работы со стеком. Для этого класса будет перегружен оператор индекса [], возвращающий значение элемента с заданным номером, а также две функции для изменения размера массива: push(), позволяющая добавить новый элемент в конец массива, и pop(), забирающая из массива последний добавленный элемент. При создании объекта для массива будет резервироваться память, для чего конструктору будет передаваться параметр capacity — ёмкость массива, т. е. максимально допустимое число элементов.

#include <iostream>
#include <math.h>
using namespace std;
class general_error
{
public :
	char *message;
	general_error ( char* message ) { this->message=message; }
};
class out_of_range
{
public :
	size_t i;
	out_of_range ( size_t i ) { this->i= i; }
};
class overflow { };
class underflow { };
class array
{
	size_t size; //реальный размер массива
	size_t capacity; //максимально-допустимый размер
	double *data;
public :
	array ( size_t capacity );
	˜ array ( );
	double operator [ ] ( size_t i ); //получить значение i-го элемента
	void push ( double v ); //добавить элемент
	double pop ( ); //убрать последний добавленный элемент
};
array::array ( size_t capacity )
{
	if ( capacity==0)
throw general_error ( "массив нулевой вместимости" );
	this->capacity=capacity;
	size =0;
	data.new double [ capacity ];
}
array::˜ array ( )
{
	delete [ ] data;
}
double array::operator [ ] ( size_t i )
{
	if ( i < size ) return data[ i ];
	else throw out_of_range ( i );
}
void array::push ( double v )
{
	if ( size < capacity ) data[size++]=v;
	else throw overflow ( );
}
double array::pop ( )
{
	if ( size > 0 ) return data [-- size ];
	else throw underflow ( );
}
main ( )
{
	char c;
	size_t i;
	double v;
	cout << "Введите ёмкость массива: ";
	cin >> v;
	array a( v );
	for (;; )
	{
		cout << "Введите \ " + \ " для добавления элемента," " \ " - \ " для извлечения, \" i \" для
		просмотра " " i-го элемента, \" a \" для выхода: ";
		cin >> c;
		try
		{
			switch ( c )
			{
			case " + " :
				cout << "Введите добавляемое число: ";
				cin >> v;
				a.push ( v );
				break;
			case " - " :
				v=a.pop ( );
				cout << "Извлечено число " << v << endl;
				break;
			case " i " :
				cout << "Введите индекс: ";
				cin >> i;
				v=a[ i ];
				cout << "Искомый элемент равен " << v << endl;
				break;
			case " a::
				return 0;
				break;
			}
		}
		catch ( const out_of_range& e )
		{
			cout << "Попытка доступа к элементу с недопустимым индексом "<< e.i << endl;
		}
		catch ( overflow )
		{
			cout << "Операция не выполнена, так как массив переполнен\n ";
		}
		catch ( underflow )
		{
			cout << "Операция не выполнена, так как массив пуст\n ";
		}
	}
}

В этом примере использованы четыре класса-индикатора исключений: general_error для ошибок неопределённого типа (класс содержит строку message, описывающую суть возникшей проблемы), out_of_range для выхода индекса за границу массива (свойство i предусмотрено для значения индекса), а также классы overflow для ошибки переполнения ёмкости массива и underflow для попыток удалить элемент из пустого массива. Обработчик out_of_range принимает объект класса-индикатора и сообщает пользователю, какое именно значение индекса оказалось недопустимым. Диалог с пользователем ведётся в бесконечном цикле, на каждой итерации которого предлагается выбрать одно из четырёх действий: добавление элемента, удаление элемента, просмотр элемента с заданным индексом или выход из программы.

10.5.3 Иерархия исключений

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

class general_error { };
class out_of_range : public general_error { };
............. .
try {...............}
catch ( out_of_range )
{ cout << "Выход индекса за границу массива\n "; }
catch ( general_error )
{ cout << "Общий сбой в работе программы\n "; }
catch (...) { cout << "Неизвестная ошибка\n "; }

В приведённом схематичном примере мы объявили два различных класса- индикатора, один базовый, для исключений общего типа, и один производный от него, для исключительной ситуации типа "недопустимый индекс при обращении к в массиву". Если бы порядок следования обработчиков был другим, обработчик индикатора out_of_range никогда не смог бы активироваться.

Если обработчик перехватил исключение, но обнаружил, что не сможет справиться с его обработкой, он может вызвать throw без аргументов: это передаст исключение дальше по цепочке уровней вложенности, на случай если на более высоком уровне есть обработчик, способный так или иначе решить возникшую ситуацию. Проиллюстрируем повторное возбуждение исключения, изменив пример из п. 10.5.2. Цикл обработки событий мы поместим в отдельную функцию main_loop(), принимающую в качестве аргумента ссылку на массив. Соответственно, создание объекта массива и передачу его в цикл обработки событий поместим в ещё один блок try, с обработчиком, принимающим исключение типа general_error. В первую очередь это позволит корректно обрабатывать ошибку нулевой ёмкости массива. Для иллюстрации передачи повторно сгенерированного исключения из внутреннего обработчика внешнему специально добавим инструкцию throw без аргументов в обработчик события out_of_range (таким образом, выход индекса за границу массива станет фатальной ошибкой, приводящей к остановке программы). Чтобы исключение могло быть успешно перехвачено на внешнем уровне вложенности, сделаем класс general_error родительским для остальных классов-индикаторов исключений.

#include <iostream>
using namespace std;
class general_error
{
public :
	char *message;
	general_error ( char* message ) { this->message=message; }
};
class out_of_range : public general_error
{
public :
	size_t i;
	out_of_range ( size_t i );
};
out_of_range::out_of_range ( size_t i )
: general_error ( "Выход индекса за границу массива" )
{ this->i= i; }
	class overflow : public general_error
{
public :
	overflow ( );
};
overflow::overflow ( ) : general_error ( "Переполнение массива" ) {}
class underflow : public general_error
{
public :
	underflow ( );
};
underflow::underflow ( ) : general_error ( "Массив пуст" ) {}
class array
{
	size_t size; //реальный размер массива
	size_t capacity; //максимально-допустимый размер
	double * data;
public :
	array ( size_t capacity ) throw ( general_error );
	˜ array ( );
	double operator [ ] ( size_t i ) throw ( out_of_range ); //получить значение i-го
	элемента
	void push ( double v ) throw ( overflow ); //добавить элемент
	double pop ( ) throw ( underflow ); //убрать последний добавленный элемент
};
array::array ( size_t capacity ) throw ( general_error )
{
	if ( capacity==0) throw
general_error ( "массив нулевой вместимости" );
	this->capacity=capacity;
	size =0;
	data.new double [ capacity ];
}
array::˜ array ( ) {
	delete [ ] data;
}
double array::operator [ ] ( size_t i ) throw ( out_of_range )
{
	if ( i < size ) return data[ i ];
	else throw out_of_range ( i );
}
void array::push ( double v ) throw ( overflow )
{
	if ( size < capacity ) data[size++]=v;
	else throw overflow ( );
}
double array::pop ( ) throw ( underflow )
{
	if ( size > 0 ) return data [-- size ];
	else throw underflow ( );
}
void main_loop ( array& a )
{
	char c;
	double v;
	size_t i;
	for (;; )
	{
		cout << "Введите \ " + \ " для добавления элемента, "
		" \ " - \ " для извлечения, \" i \" для просмотра "
		" i-го элемента, \" a \" для выхода: ";
		cin >> c;
		try {
			switch ( c ) {
			case " + " :
				cout << "Введите добавляемое число: ";
				cin >> v;
				a.push ( v );
				break;
			case " - " :
				v=a.pop ( );
				cout << "Извлечено число " << v << endl;
				break;
			case " i " :
				cout << "Введите индекс: ";
				cin >> i;
				v=a[ i ];
				cout << "Искомый элемент равен " << v << endl;
				break;
			case " a::
				return;
				break;
			}
		}
		catch ( out_of_range& e )
		{
			out << "Попытка доступа к элементу с недопустимым индексом " << e.i << endl;
			throw;
		}
		catch ( overflow )
		{
			cout<<"Операция не выполнена, так как массив переполнен\n ";
		}
		catch ( underflow ) {
			cout << "Операция не выполнена, так как массив пуст\n ";
		}
	}
}
main ( )
{
	double v;
	try
	{
		cout << "Введите ёмкость массива: ";
		cin >> v;
		array a( v );
		main_loop ( a );
	}
	catch ( general_error& e )
	{
		cout << "Произошла неустранимая ошибка следующего типа: " << e.message << endl;
	}
}

10.5.4 Спецификация исключений

Если некоторая функция содержит инструкции throw, в её заголовке можно явно прописать, какие исключения она может генерировать:

void f() throw (x,y,z);

В примере функция f() может генерировать исключения с классами- индикаторами x, y, z (и производными от них). Если заголовок описан как "void f() throw ()" — функция не генерирует исключений вообще. И, наконец, если в заголовке функции ничего не указано, она может генерировать любые исключения (без этого последнего правила не смогли бы работать программы, не использующие обработку исключений).

Если функция попытается сгенерировать исключение, отсутствующее в её списке — вызовется специальная функция void unexpected(), которая в свою очередь вызовет фунцкию void terminate(), а та вызовет функцию abort(), аварийно завершающую программу. Программист имеет возможность заменить функцию unexpected() или функцию terminate() — или сразу обе — на свои собственные, изменив таким образом обработку неспецифицированных исключений. Для такой замены нужно вызвать специальные библиотечные функции set_unexpected() и set_terminate(), передав им адреса новых функций в качестве аргументов. Наглядно можно увидеть действие этих функций в примере из раздела 10.4.2, в котором не перехватывается исключение general_error. Это исключение выбрасывается в примере конструктором класса array в том случае, если пользователь попытается создать массив нулевой ёмкости. Оно оказывается необработанным, т. к. создание массива находится за пределом блока try. К сожалению, стандартная функция не может знать о структуре класса-индикатора, и потому текстовое сообщение, оставленное конструктором, оказывается невостребованным. Программа сообщает имя неперехваченного класса-индикатора, после чего выполняет аварийное завершение работы.

10.5.5 Стандартные классы — индикаторы исключений

Стандартная библиотека C++ содержит иерархию стандартных классов- индикаторов исключений (рис. 10.5), объявленных в заголовочном файле stdexcept. Эти индикаторы можно использовать в собственных программах.

Предопределённые индикаторы исключений


увеличить изображение

Рис. 10.5.  Предопределённые индикаторы исключений

Назначение каждого класса-индикатора представлено в табл. 10.3.

Таблица 10.3. Стандартные классы-индикаторы исключений
ИсключениеОписание
exceptionбазовый класс для всех стандартных исключений C++
bad_allocнеудача выделения памяти; может генерироваться оператором new
bad_castошибка динамического приведения типов, генерируется dynamic_cast
bad_exceptionпредназначено для обработки непредусмотренных исключений в программе
bad_typeidгенерируется оператором typeid (оператор, возвращающий имя типа, которому принадлежит аргумент), если не удаётся определить тип объекта
logic_errorисключение, связанное с ошибкой в логике работы программы, которая, теоретически, может быть обнаружена при чтении кода
domain_errorгенерируется при выходе из математической области допустимых значений
invalid_argumentгенерируется при получении недопустимого аргумента
length_errorгенерируется при попытке создания слишком длинной строки
out_of_rangeвыход индекса за допустимую границу
runtime_errorисключение, связанное с ошибкой, которая, теоретически, не может быть обнаружена при чтении кода
overflow_errorгенерируется при обнаружении математического переполнения
range_errorгенерируется при попытке сохранить значение, выходящее за границы допустимого диапазона
underflow_errorгенерируется при математической ошибке исчезновения порядка

Базовый класс exception содержит конструктор по умолчанию, конструктор копирования, деструктор, перегруженный оператор присваивания, а также единственный метод what(), возвращающий ASCIIZ-строку с человеко-читаемым описанием исключительной ситуации. Классы-потомки могут добавлять к этому свой собственный функционал, в зависимости от типа ошибок, для обработки которых они предназначены. Однако стандартные классы-индикаторы по существу являются простыми обёртками над exception, ограничиваясь возможностью указать конструктору класса-индикатора сообщение, которое должен возвращать метод what().

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

#include <string>
#include <stdexcept>
#include <iostream>
using namespace std;
int main ( )
{
	//перехват выхода индекса за границу массива:
	try
	{
		strings ( " sdf " );
		s.replace ( 100, 1, 1, " c " );
	}
	catch ( out_of_range &e )
	{
		cout << "Обнаружен выход индекса за границу массива: " << e.what ( ) << endl;
	}
	catch ( exception &e )
	{
		cout << "Обнаружена ошибка неопределённого вида: " << e.what ( ) << endl;
	}
	catch (...)
	{
		cout << "Неизвестная ошибка\n ";
	}
	//перехват ошибки, возникающей в момент выполнения:
	try
	{
		throw run time_error ( "ошибка в ходе выполнения" );
	}
	catch ( run time_error &e )
	{
		cout << "Обнаружена ошибка при выполнении программы: " << e.what ( ) << endl;
	}
	catch ( exception &e )
	{
		cout << "Обнаружена ошибка неопределённого вида: " << e.what ( ) << endl;
	}
	catch (...)
	{
		cout << "Неизвестная ошибка\n ";
	}
	return 0;
}

В первом случае исключение с индикатором out_of_range генерируется автоматически, когда мы допускаем выход индекса за границу строки (такая возможность реализована классом string из стандартной библиотеки). Во втором случае исключительную ситуацию с индикатором runtime_error (обозначающим ошибку, возникающую в момент выполнения программы) мы порождаем принудительно, передавая конструктору объекта — индикатора исключения строку с описанием подробностей.

10.6 Шаблоны классов

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

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

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

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

Рассмотрим в качестве простого примера уже знакомый класс point, который хранит пару координат точки и на этот раз имеет метод info(), выводящий координаты на экран.

#include<iostream>
using namespace std;
template <class Type>
class point
{
	Type x, y;
	//.. .
	public :
	point ( Type x, Type y ) { this->x=x; this->y=y; }
	void info ( );
};
template <class Type>
void p o int<Type >::info ( )
{
	cout << "Координаты точки: x = " << x << ", y = " << y << endl;
}
main ( )
{
	point<float > f ( 10.1, 20.5 );
	f.info ( );
}

Как видим, конкретный тип (в нашем случае float) мы указали при создании объекта в угловых скобках. Точно так же мы могли указать любой стандартный тип данных, а могли — пользовательский тип, объявленный в программе. Однако необходимо помнить, что шаблоны функций и шаблоны классов могут работать только для тех типов данных (в т. ч. классов), которые поддерживают необходимые операции. Например, если мы захотим создать экземпляр класса point для хранения пары объектов какого-то собственного класса X, этот класс должен содержать конструктор копирования, а также поддерживать перегрузку двух использованных в point операторов:

class X
{
	..... .
public :
	X(X &); //конструктор копирования
	friend ostream& operator<<(X &);
	.....
};

Как упоминалось, в качестве параметров шаблона можно передавать и константы. Рассмотрим пример, где константа передаётся шаблонному классу square_matrix, хранящему квадратную матрицу заданной размерности. Такой шаблон позволит легко создавать объекты типов "матрица 20х20 целых чисел", или "матрица 10х10 типа double".

#include <iostream>
using namespace std;

template <class Type, int n>
class square_matrix
{
	Type * data;
public :
	square_matrix ( ) { data.new Type [ n*n ]; }
	void print ( );
//.. .
};
template <class Type, int n>
void square_matrix<Type, n >::print ( )
{
	for ( int i =0; i<n; i++)
	{
		for ( int j =0; j<n; j++)
		{
			cout << data[ i *n+j ] << " \t ";
		}
	cout << endl;
	}
}
int main ( )
{
	cout << "Матрица 5х5 целых чисел: \n ";
	square_matrix<int, 5> m1.
	m1.print ( );
	cout << "Матрица 10х10 значений типа double: \n ";
	square_matrix<double, 10> m2;
	m2.print ( );
	return 0;
}

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

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

10.6.1 Типаж шаблона

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

template<class Type>
Type min ( Type a,Type b )
{
	return a<b? a:b;
}
char *min ( char *a, char *b )
{
	strcmp ( a,b )<0?a:b;
}

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

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

template <class Type>
class matrix
{
	//.. .
};
template<>
class matrix<long>
{
	//.. .
};
template<>
class matrix<int>
{
	//.. .
};
...

Фактически это означает несколько раз переписать класс заново.

Ситуация ещё более осложняется, если разработчик класса хочет оставить возможность для использования в нём новых типов элементов (без переписывания класса с нуля).

В такой ситуации всё, что зависит от изменений типа данных, собирают в одном месте и называют типажом (англ. "trait"). Типаж передают классу как ещё один параметр шаблона. Типаж является небольшим по объёму классом, содержащим все операции, зависящие от типа данных, и при необходимости его несложно переписать под нужный тип. Более того, поскольку для большинства типов никакого специфического поведения не требуется, в типаж сам по себе может быть шаблоном класса. В этом случае как дополнительный параметр шаблона будет передаваться шаблонный класс, основанный на том же типе, что передан в качестве основного параметра — кроме тех редких случаев, когда требуется реализовать специфическое поведение:

template<class Type>
class MatrixTraits
{
	//.. .
};
template<>
class MatrixTraits <int>
{
	//.. .
};
template<class Type, class Traits >
class matrix
{
	//.. .
};
...
matrix <int, MatrixTraits <int>> m1.

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

10.6.2 Пример реальной иерархии шаблонов

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

Изначально библиотека потокового ввода-вывода действительно представляла собой такую иерархию классов, которая изображена на рис. 10.4. Однако по мере увеличения спроса на приложения, работающие с текстом сразу на нескольких языках, встал вопрос о поддержке кодировки Unicode, позволяющей совмещать в одной строке символы разных национальных алфавитов. В зависимости от языка, символ в Unicode может кодироваться различным количеством байт — от одного до четырёх. В C++ для поддержки таких символов существует тип wchar_t (от англ. wide characters — "широкие символы"). Фактически понадобилось создать иерархию классов, аналогичную классам iostream, но работающих с типом данных wchar_t вместо char. В итоге библиотека iostream была переработана на основе механизма шаблонов.

Классы, основанные на шаблонах, носят имена, аналогичные описанным в разделе 10.4.6, с добавлением приставки "basic" и знака подчёркивания: basic_ios, basic_istream, basic_ostream и т. д. Привычные программисту имена классов для работы с символами типа char (как, впрочем, и с wchar_t) реализованы через подстановку имени типа в конструкции typedef:

typedef basic_ios <char> ios;
typedef basic_ios <wchar_t> w ios;
typedef basic_istream<char> istream;
typedef basic_istream<wchar_t> w istream;
..... .

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

Хотя по виду конструкции typedef может показаться, что шаблоны библиотеки потокового ввода-вывода имеют один параметр, на самом деле это не совсем так. Второй параметр — это как раз типаж символов, т. е. отдельный шаблонный класс, который реализует базовые операции с символами и строками для заданного типа символов. Эти базовые операции — присваивание символов, копирование и сравнение их последовательностей, приведение к целому типу и др. Данный параметр имеет значение по умолчанию, и потому может не использоваться при объявлении экземпляров шаблона. В оригинале же шаблоны классов потокового ввода-вывода выглядят следующим образом (в виду однотипности, приведём по одному примеру для стандартного ввода-вывода, работы с файлами и со строками):

template <class charT, class traits=char_traits <charT> > basic_istream;
template <class charT, class traits=char_traits <charT> > basic_ifstream;
template <class charT, class Traits = char_ Traits <charT >, class allocator =
allocator <charT> > basic_istringstream;

Именно эти потоковые шаблоны определяют на самом деле методы для разбора и форматирования, являющиеся перегруженными версиями операторов ввода operator>> и вывода operator<<.

Аналогично реализованы шаблоны для потоковых буферов:

template <class charT, class traits = char_traits <charT> > basic_streambuf;
template <class charT, class traits = char_traits <charT> > basic _filebuf;

10.7 Элементы стандартной библиотеки C++

10.7.1 Базовые понятия

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

В библиотеке выделяют пять основных компонентов:

10.7.2 Контейнеры

Будем считать, что стандартная библиотека реализует следующие контейнеры:

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

stack<int> s;

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

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

Есть также и необязательные операции: front, back, push_front, push_back, pop_front, pop_back, и оператор [].

10.7.3 Итераторы

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

В C++ есть несколько разных типов итераторов (табл. 10.4):

Таблица 10.4. Типы итераторов
ИтераторОписание
input_iterator(для чтения)Читают значения с движением вперёд. Могут быть инкрементированы, сравнены и разыменованы.
output_iterator(для записи)Пишут значения с движением вперёд. Могут быть инкрементированы и разыменованы
forward_iterator(однонаправленные)Читают или пишут значения с движением вперёд. Комбинируют функциональность предыдущих двух типов с возможностью сохранять значение итератора
bidirectional_iteratorдвунаправленные)Читают и пишут значения с движением вперёд или назад. Похожи на однонаправленные, но их также можно инкрементировать и декрементировать
random_iterator(с произвольным доступом)Читают и пишут значения с произвольным доступом. Самые мощные итераторы, сочетающие функциональность двунаправленных итераторов и возможность выполнения арифметики указателей и сравнений указателей
reverse_iterator(обратные)Или итераторы с произвольным доступом, или двунаправленные, движущиеся в обратном направлении.

Итераторы обычно используются парами: один для указания текущей позиции, а второй для обозначения конца коллекции элементов. Итераторы создаются объектом-контейнером, используя стандартные методы begin() и end(). Функция begin() возвращает указатель на первый элемент, а end() — на воображаемый несуществующий элемент, следующий за последним.

К элементам контейнера — например, vector — можно обращаться и по номерам, как к элементам классического массива — и с помощью итераторов:

Необъектный подходПравильный (объектный) подход
#include <iostream>
#include <vector>
using namespace std;
main ( )
{
	vector<int> a;
	//добавляем элементы
	a.push_back ( 1 );
	a.push_back ( 4 );
	a.push_back ( 8 );
	for ( in t y=0;y<a.size ( ); y++)
	{
		//выводим 1 4 8
		cout<<a [ y]<<"";
	}
}
#include <iostream>
#include <vector>
using namespace std;
main ( )
{
	vector <int> a;
	vector <int >::iterator it;
	//добавляем элементы
	a.push_back ( 1 );
	a.push_back ( 4 );
	a.push_back ( 8 );
	for ( it=a.begin ( ); it !=a.end ( ); it ++)
	{
		//выводим 1 4 8
		cout<<*it <<" ";
	}
}

В отличие от счётчика цикла, итератор обеспечивает доступ к элементу, а не только его перебор. В отличие от операции индексации, итератор "не портится" при добавлении в контейнер новых элементов. Кроме того, индексация иногда вообще неприменима — например, в коллекциях с медленным произвольным доступом, таких как списки (list).

Рассмотрим, как использовать контейнеры на примере класса vector:

#include <iostream>
#include <vector >
#include <algorithm>
using namespace std;
main ( )
{
	//Объявляем вектор из целых чисел
	vector <int> k;
	//Добавляем элементы в конец вектора
	k.push_back ( 22 );
	k.push_back ( 11 );
	k.push_back ( 4 );
	k.push_back ( 100 );
	vector <int >::iteratorp;
	cout << "Вывод неотсортированного вектора:\n ";
	for ( p = k.begin ( ); p<k.end ( ); p++) {
	cout << *p << " ";
	}
	//Сортировка вектора.
	sort ( k.begin ( ), k.end ( ) );
	cout << " \nВывод отсортированного вектора:\n ";
	for ( p = k.begin ( ); p<k.end ( ); p++)
	{
	cout << *p << " ";
	}
	cout << endl;
}

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

Вывод неотсортированного вектора:
22 11 4 100
Вывод отсортированного вектора:
4 11 22 100

10.7.4 Алгоритмы

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

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

Многие алгоритмы получают в качестве параметра различные функции. Эти функции используются для сравнения элементов, их преобразования и т. д. Однако вызов функции по указателю — ресурсоёмкая операция. Вместо указателя на функцию можно передать объект любого класса с перегруженным оператором вызова функции operator(). Одно из преимуществ обращения к переопределённому оператору вызова функции вместо собственно её вызова заключается в том, что переопределённый оператор может быть реализован в классе как встроенная функция, т. е. код оператора будет подставлен вместо вызова.

Рассмотрим пример алгоритма find, который находит первое вхождение заданного значения в коллекцию. Алгоритм принимает в качестве аргументов пару итераторов и искомое значение; соответственно возвращается итератор, указывающий на первое вхождение заданного значения. Благодаря универсальности механизма итераторов, алгоритм будет работать со структурой любого типа, в том числе и с обычными массивами языка C. Например, чтобы найти первое вхождение числа 7 в массив целых, требуется выполнить следующий код, использующий в качестве итераторов обычные указатели:

int data[100];
...
int * where;
where = find ( data, data +100, 7 );

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

vector <int> a;
...
vector <int >::iterator where;
where = find ( a.begin ( ), a.end ( ), 7 );

10.8 Задачи для самостоятельного решения

10.8.1 Иерархия классов

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

  1. "Водный транспорт", "Грузовое судно"
  2. "Летательный аппарат", "Дирижабль"
  3. "Здание", "Коттедж"
  4. "Двигатель", "Двигатель внутреннего сгорания"
  5. "Устройство печати", "Струйный принтер"
  6. "Устройство ввода", "Цифровая камера"
  7. "Растровое изображение", "Репродукция картины"
  8. "Млекопитающее", "Собака"
  9. "Транспортное средство", "Легковой автомобиль"
  10. "Печатное издание", "Номер журнала"
  11. "Документ", "Квитанция об оплате"
  12. "Пищевой продукт", "Йогурт"
  13. "Корпусная мебель", "Книжный шкаф"
  14. "Проверка знаний", "Экзамен"
  15. "Носитель информации", "Компакт-диск"
  16. "Аудиозапись", "файл в формате MP3"
  17. "Видеозапись", "Художественный фильм"
  18. "Транспортное средство", "Маршрутный автобус"
  19. "Средство связи", "Сотовый телефон"
  20. "Человек", "Член клуба"
  21. "Птица", "Почтовый голубь"
  22. "Электронная карта", "Абонемент на проезд"
  23. "Дата", "День рождения"
  24. "Удостоверение", "Паспорт"
  25. "Сотрудник компании", "Начальник отдела"

10.8.2 Перегрузка операторов

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

  1. "–" (вычитание одной коллекции из другой), класс "множество символов"
  2. "+" (объединение коллекций), класс "множество целых чисел"
  3. "*" (пересечение коллекций), класс "множество целых чисел"
  4. "!=" (сравнение коллекций на неравенство), класс "неупорядоченный массив вещественных чисел"
  5. "==" (сравнение коллекций на неравенство), класс "упорядоченный массив символов"
  6. "[]" (получение элемента по его номеру в коллекции), класс "неупорядоченный массив целых чисел"
  7. "[]" (получение элемента по его номеру в коллекции), класс "упорядоченный массив вещественных чисел"
  8. "%" (проверка элемента на принадлежность коллекции), класс "множество целых чисел"
  9. "%" (проверка элемента на принадлежность коллекции), класс "упорядоченный массив символов"
  10. "<<" (удаление элемента из коллекции с его выводом на экран), класс "множество целых чисел"
  11. ">>" (добавление введённого с клавиатуры элемента в коллекцию), класс "множество символов"
  12. ">=" (проверка на включение коллекции, заданной вторым аргументом, в начальную часть коллекции, заданной первым аргументом), класс "упорядоченный массив символов"
  13. "<=" (проверка на включение коллекции, заданной первым аргументом, в начальную часть коллекции, заданной вторым аргументом), класс "неупорядоченный массив символов"
  14. "++" (добавление элемента со значением, на единицу больше последнего добавленного элемента), класс "упорядоченный массив целых чисел"
  15. "++" (добавление элемента со значением, на единицу больше последнего добавленного элемента), класс "стек целых чисел"
  16. "--" (удаление последнего добавленного элемента), класс "упорядоченный массив целых чисел"
  17. "--" (удаление элемента), класс "очередь вещественных чисел"
  18. "--" (опустошение коллекции), класс "множество вещественных чисел"
  19. ">>" (добавление введённого с клавиатуры элемента в коллекцию), класс "очередь целых чисел"
  20. "<<" (удаление элемента из коллекции с его выводом на экран), класс "очередь вещественных чисел"
  21. ">>" (добавление введённого с клавиатуры элемента в коллекцию), класс "стек символов"
  22. "<<" (удаление элемента из коллекции с его выводом на экран), класс "стек целых чисел"
  23. "*" (умножение всех элементов коллекции на заданное число), класс "неупорядоченный массив вещественных чисел"
  24. "/" (деление всех элементов коллекции на заданное число), класс "множество вещественных чисел"
  25. "˜" (смена регистра), класс "множество символов"

10.8.3 Обработка исключительных ситуаций

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

Лекция 11. Знакомство с Qt. Подготовка к работе

Рассказывается о кроссплатформенном инструментарии разработки Qt, политике его лицензирования и настройках.

11.1 Знакомство с Qt. Обзор истории

Кроссплатформенный инструментарий разработки Qt появился впервые в 1995 году благодаря своим разработчикам Хаарварду Норду и Айрику Чеймб- Ингу. С самого начала создавался как программный каркас, позволяющий создавать кроссплатформенные программы с графическим интерфейсом. Первая версия Qt вышла 24 сентября 1995. Программы, разработанные с Qt, работали как под управлением операционных систем семейства Microsoft Windows™ так и под управлением Unix-подобных систем.

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

В июне 1999 года вышла вторая версия — Qt 2.0. А в 2000 году состоялся выпуск версии для встраиваемых систем, который назывался Qt Embedded. Версия Qt 3.0 — 2001 год — работала в ОС семейства Windows™ и многих Unix-подобных ОС, таких как MacOS, xBSD, в различных вариантах Linux для персональных компьютеров и встраиваемых систем. Он имел 42 дополнительных класса, объём вырос до более чем 500 000 строк кода. Летом 2005 года состоялся выпуск Qt 4.0, который включал в совокупности около 500 классов и имел огромное количество существенных улучшений. Вместе с выпуском Qt 4.5 вышло и специализированная интегрированная среда разработки QtCreator.

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

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

Инструментарий разработки Qt используют для создания кроссплатформенных программ. Здесь под этим утверждением мы подразумеваем программы, исходный текст которых можно скомпилировать на разных программных платформах (различные разновидности Linux, Windows, MacOS и т.д.) практически без изменений или с незначительными изменениями. Кроме того Qt используют и для разработки программ, имеющих характерный ("родной", native) для программного окружения или даже собственный стилизованный интерфейс. Всё это благодаря открытому свободному программному коду, удобному и логическому API и широким возможностям применения.

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

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

Программный код, зависящий от оконной системы в Qt5, был отделён и реорганизован в отдельные библиотеки расширения, что позволило упростить перенос Qt на новые платформы и адаптации для поддержки других оконных систем. Благодаря QPA (Qt Platform Abstraction) в Qt5 реализована поддержка многих платформ для мобильных устройств.

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

11.1.1 Основные составляющие Qt

Рассмотрим основные составляющие кроссплатформенного средства разработки Qt: модули и инструменты.

На рис. 11.1 изображены основные составляющие Qt. Модули и инструменты доступны для разработки под целевые (Reference) и другие (Other) платформы. Средства Qt разделены по назначению на отдельные части — модули. Каждый из модулей выполнен в виде отдельной библиотеки. Разработчик имеет возможность выбрать модули, которые он использует в программе. Модули имеют взаимозависимости: одни модули используют возможности, которые предоставляют другие. Основу составляют основные (Essentials) модули:

Состав Qt5


увеличить изображение

Рис. 11.1.  Состав Qt5

Существует также много дополнительных (Add-On) модулей. Стоит заметить, что разделение на основные и дополнительные модули характерно Qt5 в отличие от предыдущих версий. Названия некоторых модулей в Qt5 по сравнению с Qt4 были изменены, а некоторые средства были вынесены в отдельные или перенесены в другие модули. Эти изменения необходимо учитывать при переносе программ, которые были разработаны с использованием Qt4. Почти все примеры, которые мы будем рассматривать, работают как с Qt4 так и Qt5. В случаях, когда это существенно, мы будем указывать на отличия.

Кроме модулей, в состав инструментария входят инструменты разработки, исходные тексты Qt, примеры программ и документация.

11.2 Лицензирование Qt

Qt распространяется по условиям трёх различных лицензий: GNU GPL v3, GNU LGPL v3 и по коммерческой лицензии компании Digia. Здесь мы лишь кратко осмотрим основные положения этих лицензий и что это означает для программ, которые используют соответственно лицензированный Qt.

11.2.1 GPL

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

11.2.2 LGPL

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

11.2.3 Commercial

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

11.3 Справка и ресурсы

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

Для просмотра интегрированной справки можно воспользоваться как средой Qt Creator, так и специальной отдельной программой, которая называется Qt Assistant и является частью инструментария Qt.

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

В режиме справки или в случае использования Qt Assistant слева от окна документации расположена панель, которая может переключаться в несколько различных режимов: Закладки (Bookmarks), Содержание (Contents), Указатель (Index) и Поиск (Search). Режим панели определяется выпадающим списком сверху. Особенно удобно пользоваться режимом Указатель (Index) при работе: как только пользователь вводит начало названия класса, метода или статьи, в справке выполняется поиск и отображение совпадений. Это особенно пригодится для быстрой навигации и поиска в справке.

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

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

11.4 Обзор настроек среды Qt Creator

Для разработки программ с использованием библиотеки Qt была создана интегрированная среда разработки Qt Creator. Её первая версия была представлена одновременно с официальным выпуском Qt 4.5.0. Это полноценная кроссплатформенная среда для создания новых проектов и работы с ними.

Мы рассмотрим работу со средой Qt Creator версии 3.3.0, которая позволяет управлять целым рядом этапов разработки программы такими как: управление сеансами и проектами, редактирование и создание программного кода, конструирование пользовательского интерфейса программы, анализ быстродействия, анализ использования ресурсов, отладка, построение проекта, запуск программы.

Одно из первых действий, которое необходимо выполнить разработчику перед началом работы с Qt Creator — это настроить среду таким образом, чтобы с ней было удобно работать. Конечно, Qt Creator имеет стандартные настройки, которые уже достаточно удобны для работы. Несмотря на это, мы хотели бы обратить ваше внимание на некоторые настройки, которые особенно полезны в работе. Среди большого количества настроек мы рассмотрим лишь наиболее важные для работы, а именно настройки компиляции и настройки редактора кода. Для доступа к диалогу настройки используем главное меню (пункт Tools- > Options..., см. рис. 11.2).

Сначала коснёмся настроек компиляции. Для управления настройками, относящимися к построению проекта, Qt Creator использует понятие комплекта (Kit). Комплект (Kit) — это конфигурация, которую составляют версия Qt, компилятор и ещё некоторые дополнительные настройки. Таким образом, Qt Creator позволяет работать с несколькими различными версиями Qt, несколькими компиляторами в системе, выбирать и настраивать их комбинацию для построения проекта.

Стандартным для Linux и Mac OS X является компилятор GCC. Для Windows можно воспользоваться его свободным аналогом — MinGW, или компилятором MSVC, который входит в состав Microsoft Windows SDK или Visual Studio (SDK для Windows 7 можно получить бесплатно на официальном сайте Microsoft).

Если среда Qt Creator была установлена отдельно, то возможно необходимо будет добавить комплект самостоятельно. Для этого необходимо выполнить следующие шаги:

  1. Перейдите на вкладку Compilers (Компиляторы) раздела Build and Run (Сборка и запуск) и проверьте наличие доступных компиляторов. Обычно наличие компиляторов MSVC (Windows) и GCC (Linux, MacOS) определяется автоматически. Для того, чтобы добавить компилятор MinGW, необходимо воспользоваться кнопкой Add->MinGW(Добавить->MinGW). Затем для добавленного компилятора ввести имя (поле Name) и указать полный путь к компилятору C++ — g++ (поле Compiler path);
  2. Перейдите на вкладку Qt Versions (Версии Qt) и проверьте наличие доступных версий Qt. Установленную версию можно легко добавить в список воспользовавшись кнопкой Add...(Добавить). Для добавленной версии укажите имя (поле Version name) и полный путь к программе qmake (поле qmake location). Обычно данная программа содержится в папке bin в месте, куда был установлен Qt;
  3. Перейдите на вкладку Kits (Комплекты). Добавьте новый комплект с помощью кнопки Add (Добавить). Выделите в списке новый комплект и задайте для него комбинацию из установленных компилятора (выпадающий список Compiler) и версии Qt (выпадающий список Qt Version). Далее задайте имя инструментария (поле Name) и сохраните изменения.

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

Окно диалога настроек Qt Creator


увеличить изображение

Рис. 11.2.  Окно диалога настроек Qt Creator

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

Таблица 11.1. Некоторые важные горячие клавиши Qt Creator
Комбинация клавишОписание
EscВыполняет переход к редактированию кода. Несколько последовательных нажатий этой клавиши переключают пользователя в режим редактирования, закрывают панели вывода справки, отладки.
F4Переключает редактор между файлом реализации (.сpp) и соответствующим заголовочным файлом (.h), которые содержат объявления интерфейса и реализации класса соответственно.
F2Выполняет переход к месту объявления переменной, функции, класса, на имени которых стоял курсор при нажатии.
F1Показывает справку для класса или метода Qt, на имени которого стоит курсор.
Ctrl+Shift+RПереименование переменной, метода, класса, на имени которых стоит курсор. Имя будет изменено во всех местах, где встречается его использование: не только в текущем файле, но и в других файлах проекта. При замене имени будет учитываться область видимости имени, поэтому замена произойдёт только в местах обращения к имени. Именно этим это действие отличается от обычного поиска и замены текста
Ctrl+Shift+UПоиск всех мест обращения к переменной, методу, классу на имени которого стоит курсор.
Ctrl+KОткрывает поле быстрого поиска (Locator).
Alt+EnterПозволяет открыть доступные дополнительные действия для переменной, метода, класса, оператора в позиции курсора. Это дополнительные действия для рефакторинга (реорганизации и улучшения существующего кода) могут содержать изменение порядка параметров, изменения в текущем фрагменте кода, добавление фрагментов кода и т.д
Ctrl+SpaceВызывает выпадающий список автозавершения кода.
Ctrl+FПоиск текста в текущем открытом файле.
Ctrl+Shift+FРасширенный поиск текста в файле, проекте или группе проектов (доступны дополнительные настройки).

11.4.1 Первый оконный проект

Для создания простого оконного проекта выберите в главном меню File->New File or Project...(Файл->Новый файл или проект...) или нажмите Ctrl+N. На экране появится окно мастера новых файлов и проектов (см. рис. 11.3).

Для создания нашего проекта выберем раздел Applications(Приложения) в списке Projects(Проекты) и выберем Qt Widgets Application как тип проекта (приложение на Qt с использованием виджетов). Нажмём кнопку Choose...(Выбрать...).

Окно мастера новых файлов и проектов (шаг 1)


увеличить изображение

Рис. 11.3.  Окно мастера новых файлов и проектов (шаг 1)

Далее нам необходимо ввести имя проекта в поле ввода Name (например FirstGuiProject). Поле ввода Create in содержит путь, где будет создан каталог с новым проектом. Флажок Use as default project location(Использовать как место по умолчанию для размещения проектов) означает, что путь расположения проекта сохраняется и для последующих новых проектов (рис. 11.4).

Окно мастера новых файлов и проектов (шаг 2)


увеличить изображение

Рис. 11.4.  Окно мастера новых файлов и проектов (шаг 2)

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

Следующее окно мастера (см. рис. 11.6) информирует о классах, которые будут сгенерированы автоматически для нового проекта. Для главного окна программы будет сгенерирован класс MainWindow (поле Class name), который будет унаследован от класса QMainWindow (поле Base class). Класс QMainWindow обладает особенностями, характерными для главного окна программы, такими как главное меню, панель инструментов и т.п. Для кода класса MainWindow мастер создаст заголовочный файл — mainwindow.h (поле Header file) и файл реализации — mainwndow.cpp (поле Source file), а также добавит их в проект.

Окно мастера новых файлов и проектов (шаг 3)


увеличить изображение

Рис. 11.5.  Окно мастера новых файлов и проектов (шаг 3)

Флажок Generate form указывает на необходимость сгенерировать и добавить к проекту файл формы для главного окна.

Окно мастера новых файлов и проектов (шаг 4)


увеличить изображение

Рис. 11.6.  Окно мастера новых файлов и проектов (шаг 4)

В завершающем окне мастера (см. рис. 11.7) нажмите Finish (Завершить).

Файлы формы позволяют редактировать вид окна с помощью визуального редактора интерфейса — Qt Designer. Оболочка Qt Creator также даёт возможность редактировать файлы формы. Файлы формы главного окна будет сгенерирован автоматически — mainwindow.ui (поле Form file).

Окно мастера новых файлов и проектов (шаг 5)


увеличить изображение

Рис. 11.7.  Окно мастера новых файлов и проектов (шаг 5)

После завершения работы мастера проект откроется в окне оболочки Qt Creator. В левой части окна теперь можно исследовать структуру проекта, который состоит из всех файлов, входящих в проект и были сгенерированы мастером. Для компиляции и запуска проекта нажмите кнопку запуска программы или комбинацию клавиш Ctrl+R. После запуска появится пустое окно — главное окно нашей программы. Вот таким образом выглядит создание простого оконного проекта. Все файлы проекта были сгенерированы мастером, который используют обычно для удобства и чтобы ускорить работу. Конечно, есть и другой путь — можно создать каждый из файлов отдельно и, без каких-либо трудностей, добавить к проекту. Мы подробно рассмотрим как это делать, для того, чтобы исследовать основные составляющие проекта Qt и понять как происходит его компиляция.

11.5 Задачи для самостоятельного решения

  1. Повторите описанные шаги для создания собственного проекта. Назовите проект MyGuiProject. Класс главного окна программы назовите MyMainWindow. Просмотрите структуру проекта. Скомпилируйте и запустите проект на выполнение.
  2. Попробуйте использовать любую из горячих клавиш, описанных в таблице "Некоторые важные горячие клавиши".
  3. Используйте документацию Qt и найдите описание для классов QMainWindow и QApplication.

Лекция 12. Структура проекта. Основные типы

Рассматриваются проекты в Qt, их компиляция, работа с текстовыми строками и файлами.

12.1 Файлы проекта

Теперь давайте рассмотрим из чего состоит проект Qt. В общем, проект Qt имеет такую структуру:

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

Теперь рассмотрим создание своего проектного файла. Создадим новую папку, где будет размещаться проект (например: custom_project). Создайте файл (это будет файл проекта) введите его имя с расширением .pro (например: custom_project.pro). Наш файл пока что пустой, но его уже можно открыть в Qt Creator (воспользуйтесь главным меню: File->Open File or Project...).

Создать пустой проект можно с помощью мастера построения проектов. Для этого надо воспользоваться главным меню File->New File or Project... либо комбинацией клавиш Ctrl+Shift+N. В окне мастера нужно выбрать раздел Other Project (Другой проект) и тип проекта — Empty Qt Project.

После того, как мы открыли проект, Qt Creator предлагает выбрать комплект для его компиляции. В разделе Projects (Проекты) выберем комплект по умолчанию и нажмём Configure Project. В дереве проекта выберем и откроем файл проекта. Теперь настало время исследовать синтаксис проектных файлов Qt.

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

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

# Указываем тип проекта
TEMPLATE = app # app - Applic