Вы находитесь на странице: 1из 124

Лабораторная работа № 7.

Ознакомление с рекурсиями и их
программная реализация

Цель: изучить понятия рекурсии, рекурсивные функции в


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

Теоретическая часть

В окружающем нас мире часто можно встретить объекты,


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

Рис. 1. Рекурсивные графические объекты

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


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

Пример 1. В арифметической прогрессии найдите an, если известны а1 =


-2.5, d =0.4, не используя формулу n -го члена прогрессии.
По определению арифметической прогрессии, an=an-1+d, при этом
an-1=an-2+d, an-2=an-3+d,... a2=a1+d.
Таким образом, нахождение an для номера n сводится к решению
аналогичной задачи, но только для номера n -1, что в свою очередь сводится
к решению для номера n -2, и так далее, пока не будет достигнут номер 1
(значение а1 дано по условию задачи).
float arifm (int n, float a, float d) {
if (n<1) return 0; // для неположительных номеров
if (n==1) return a; // базовый случай: n=1
return arifm(n-1,a,d)+d; // общий случай
}
В рекурсивных функциях несколько раз используется return. В базовом
случае возвращается конкретный результат (в примере – значение а ), а
общий случай предусматривает вызов функцией себя же, но с меняющимися
значениями отдельных параметров (в примере изменяется только номер
члена последовательности, при этом не меняются разность и первый член
прогрессии).
В программировании выделяют прямую и косвенную рекурсию. Прямая
рекурсия предусматривает непосредственное обращение рекурсивной
функции к себе, но с иным набором входных данных. Косвенная (взаимная)
рекурсия представляет собой последовательность взаимных вызовов
нескольких функций, организованная в виде циклического замыкания на
тело первоначальной функции, но с иным набором параметров.
Для решения задач рекурсивными методами разрабатывают
следующие этапы, образующие рекурсивную триаду:
 параметризация – выделяют параметры, которые используются
для описания условия задачи, а затем в решении;
 база рекурсии – определяют тривиальный случай, при котором
решение очевидно, то есть не требуется обращение функции к себе;
 декомпозиция – выражают общий случай через более простые
подзадачи с измененными параметрами.
Целесообразность применения рекурсии в программировании
обусловлена спецификой задач, в постановке которых явно или опосредовано
указывается на возможность сведения задачи к подзадачам, аналогичным
самой задаче. При этом эффективность рекурсивного или итерационного
способов решения одной и той же задачи определяется в ходе анализа
работоспособности программы на различных наборах данных. Таким
образом, рекурсия не является универсальным способом в
программировании. Ее следует рассматривать как альтернативный вариант
при разработке алгоритмов решения задач.
Повысить эффективность рекурсивных алгоритмов часто
представляется возможным за счет пересмотра этапов триады. Например,
введение дополнительных параметров, не оговоренных в условии задачи, в
реализации декомпозиции могут быть применены другие соотношения, а
также можно организовать расширение базовых случаев с сохранением
промежуточных результатов.
Область памяти, предназначенная для хранения всех промежуточных
значений локальных переменных при каждом следующем рекурсивном
обращении, образует рекурсивный стек. Для каждого текущего обращения
формируется локальный слой данных стека (при этом совпадающие
идентификаторы разных слоев стека независимы друг от друга и не
отождествляются). Завершение вычислений происходит посредством
восстановления значений данных каждого слоя в порядке, обратном
рекурсивным обращениям. В силу подобной организации количество
рекурсивных обращений ограничено размером области памяти, выделяемой
под программный код. При заполнении всей предоставленной области
памяти попытка вызова следующего рекурсивного обращения приводит к
ошибке переполнения стека.

Пример 2. Для целого неотрицательного числа n найдите


его факториал.
Разработаем рекурсивную триаду.
Параметризация: n – неотрицательное целое число.
База рекурсии: для n =0 факториал равен 1.
Декомпозиция: n!=(n-1)!xn.
long factor (int n) {
if (n<0) return 0; // для отрицательных чисел
if (n==0) return 1; // базовый случай: n=0
return factor(n-1)*n; // общий случай (декомпозиция)
}
Рассмотрим задачи, для которых можно предложить рекурсивные
алгоритмы решения, в то время как итерационные алгоритмы были бы
сложными и искусственными.

Пример 3. Задача о коэффициентах Безу


Для любых натуральных чисел n и m найдите коэффициенты Безу, то
есть такие целые a и b, что
выполняется равенство: nod(n,m)=axn+bxm (где nod(n,m) – наибольший
общий делитель n и m ).
Параметризация.
m, n – данные натуральные числа, неизменяемые параметры;
d – наибольший общий делитель данных чисел,
неизменяемый параметр;
bm, bn – коэффициенты Безу при n и m соответственно, эти параметры
меняются при очередном рекурсивном вызове функции.
База рекурсии. Если при очередном обращении к функции с
передаваемыми параметрами выполняется равенство d=mxbm–nxbn,
то коэффициенты Безу найдены. Требуется вывести линейную комбинацию.
Декомпозиция. Если равенство не выполняется, то инкрементно
увеличиваем коэффициент при меньшем из чисел ( n или m ). Следующий
вызов рекурсивной функции выполняется с измененным набором отдельных
параметров. При этом снова проверяется база рекурсии, и
рекурсивный алгоритм повторяется (либо достигается база
и функция завершает работу, либо выполняется декомпозиционный переход).
//Коэффициенты Безу
#include "stdafx.h"
#include <iostream>
using namespace std;
int nod(int m, int n);
void bezu(int d, int m, int n, int bm, int bn);

int _tmain(int argc, _TCHAR* argv[]){


int x,y,del,buf;
printf("Задача нахождения коэффициентов Безу");
printf("\nВведите два натуральных числа:");
printf("\nX= ");
scanf("%d",&x);
printf("Y= ");
scanf("%d",&y);
if (x < y) {buf = x; x = y; y = buf;}
del=nod(x,y);
printf("\nЛинейная комбинация:\n");
bezu(del,x,y,1,1);
system("pause");
return 0;
}

//функция нахождения наибольшего общего делителя двух чисел


int nod(int m, int n){
if (m%n==0) return n;
return nod(n,m%n);
}
//функция нахождения и вывода на экран коэффициентов Безу
void bezu(int d, int m, int n, int bm, int bn){
int pm,pn;
pm = m * bm;
pn = n * bn;
//проверка базы рекурсии (выполнение линейной комбинации)
if (d == pm - pn)
printf ("%d = %d*%d - %d*%d", d, bm, m, bn, n);
//декомпозиция
else {
bn++;
pn=n*bn;
/*если произведение pm больше, чем pn, то порядок
параметров сохраняется*/
if (pm > pn) bezu(d, m, n, bm, bn);
/*если произведение pm меньше, чем pn, то порядок
параметров изменятеся*/
else bezu(d, n, m, bn, bm);
}
}

Пример 4. Задача о Ханойских башнях


Ханойская башня является одной из популярных головоломок XIX
века. Даны три стержня, на один из которых нанизаны n колец, причем
кольца отличаются размером и лежат меньшее на большем. Задача состоит в
том, чтобы перенести пирамиду из n колец за наименьшее число ходов с
одного стержня на другой. За один раз разрешается переносить только одно
кольцо, причём нельзя класть большее кольцо на меньшее.
Существует древнеиндийская легенда, согласно которой в городе
Бенаресе под куполом главного храма, в том месте, где находится центр
Земли, на бронзовой площадке стоят три алмазных стержня. В день
сотворения мира на один из этих стержней было надето 64 кольца. Бог
поручил жрецам перенести кольца с одного стержня на другой, используя
третий в качестве вспомогательного. Жрецы обязаны соблюдать условия:
1. переносить за один раз только одно кольцо;
2. кольцо можно класть только на кольцо большего размера или на
пустой стержень.
Согласно легенде, когда, соблюдая все условия, жрецы перенесут все
64 кольца, наступит конец света. Для 64 колец это 18 446 744 073 709 551 615
перекладываний, и, если учесть скорость одно перекладывание в секунду,
получится около 584 542 046 091 лет, то есть апокалипсис наступит нескоро.
На рисунке ( рис. 2) изображена ситуация, иллюстрирующая
перекладывание 7 колец со стержня А на В через вспомогательный С.

Рис. 2. Ситуация при переносе семи колец

Кольцо со стержня А можно перенести на стержень В или С, кольцо со


стержня В можно перенести на стержень С, однако, нельзя перенести его на
стержень А.
Задача состоит в том, чтобы определить последовательность
минимальной длины переноса колец. Решением задачи будем считать
последовательность допустимых переносов, каждый из которых имеет
вид: A->B, A->C, B->A, B->C, C->A, C->B. Если кольцо всего одно, то задача
решается за один перенос A->В. Для перемещения двух колец требуется
выполнить три действия: A->C, A->В, C->B. Решение задачи для трех колец
содержит семь действий, для четырех – 15.
Напишем рекурсивную функцию, которая находит решение для
произвольного числа колец.
Параметризация. Функция имеет четыре параметра,
первый параметр – число переносимых колец, второй параметр – стержень,
на который первоначально нанизаны кольца. Третий параметр функции –
стержень, на который требуется перенести кольца, и, наконец,
четвертый параметр – стержень, который разрешено использовать в
качестве вспомогательного.
База рекурсии. Перенос одного стержня.
Декомпозиция. Последовательность переноса колец изображена на
рисунке ( рис. 3).
Рис. 3. Схема решения задачи о Ханойских башнях для четырех колец
Чтобы перенести n колец со стержня A на стержень B, используя
стержень C в качестве вспомогательного, можно поступить следующим
образом:
 перенести n –1 кольцо со стержня A на C, используя стержень B в
качестве вспомогательного стержня;
 перенести последнее кольцо со стержня A на стержень B ;
 перенести n –1 кольцо со стержня C на B, используя стержень A в
качестве вспомогательного стержня.
При переносе n –1 кольца можно воспользоваться тем же алгоритмом,
т.к. на нижнее кольцо с самым большим диаметром можно просто не
обращать внимания. Перенос одного кольца в программе выражается в том,
что выводится соответствующий ход.
//Ханойские башни
#include "stdafx.h"
#include <iostream>
using namespace std;
int hanoj(int n, char A, char B, char C);
//Объявление функции перемещения колец с A на C через B

int _tmain(int argc, _TCHAR* argv[]){


char x='A',y='B',z='C';
int k,h;
printf("Задача о Ханойских башнях");
printf("\nВведите количество колец: ");
scanf("%d",&k);
h=hanoj(k,x,z,y);
printf("\nКоличество перекладываний равно %d",h);
system("pause");
return 0;
}

//Описание функции перемещения колец с A на C через B


int hanoj(int n, char A, char B, char C){
int num;
if (n == 1) {printf("\n %c -> %c", A, C); num = 1;}
else {
num=hanoj(n-1, A, C, B);
printf("\n %c -> %c", A, C);
num++;
num+=hanoj(n-1, B, A, C);
}
return num;
}
Ключевые термины
База рекурсии – это тривиальный случай, при котором решение задачи
очевидно, то есть не требуется обращение функции к себе.
Декомпозиция – это выражение общего случая через более простые
подзадачи с измененными параметрами.
Косвенная (взаимная) рекурсия – это последовательность взаимных
вызовов нескольких функций, организованная в виде
циклического замыкания на тело первоначальной функции, но с иным
набором параметров.
Параметризация – это выделение из постановки задачи параметров,
которые используются для описания условия задачи и решения.
Прямая рекурсия – это непосредственное обращение рекурсивной
функции к себе, но с иным набором входных данных.
Рекурсивная триада – это этапы решения задач рекурсивным
методом.
Рекурсивная функция – это функция, которая в своем теле содержит
обращение к самой себе с измененным набором параметров.
Рекурсивный алгоритм – это алгоритм, в определении которого
содержится прямой или косвенный вызов этого же алгоритма.
Рекурсивный стек – это область памяти, предназначенная для
хранения всех промежуточных значений локальных переменных при каждом
следующем рекурсивном обращении.
Рекурсия в программировании – это пошаговое разбиение задачи на
подзадачи, подобные исходной.
Рекурсия в широком смысле – это определение объекта посредством
ссылки на себя.

Краткие итоги
1. Свойством рекурсивности характеризуются объекты
окружающего мира, обладающие самоподобием.
2. Рекурсия в широком смысле характеризуется определением
объекта посредством ссылки на себя.
3. Рекурсивные функции содержат в своем теле обращение к самим
себе с измененным набором параметров. При этом обращение к себе может
быть организовано через цепочку взаимных обращений функций.
4. Решение задач рекурсивными способами проводится
посредством разработки рекурсивной триады.
5. Целесообразность применения рекурсии в программировании
обусловлена спецификой задач, в постановке которых явно или опосредовано
указывается на возможность сведения задачи к подзадачам, аналогичным
самой задаче.
6. Область памяти, предназначенная для хранения всех
промежуточных значений локальных переменных при каждом следующем
рекурсивном обращении, образует рекурсивный стек.
7. Рекурсивные методы решения задач нашли широкое применение
в процедурном программировании.

Задание на лабораторную работу

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


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

Задания к лабораторной работе.


Выполните приведенные ниже задания.
1. Определите закономерность формирования членов
последовательности. Найдите n -ый член последовательности: 1, 1, 2, 3, 5, 8,
13, ...
2. Составьте программу вычисления биномиального

коэффициента   для данных неотрицательных

целых  . Решите задачу двумя способами: 1


– используйте функцию вычисления факториала; 2 – выразите

вычисление   через  .
3. Исполнитель умеет выполнять два действия: "+1", "*2".
Составьте программу получения из числа 1 числа 100 за наименьшее
количество операций.
4. Найдите наибольший общий делитель двух натуральных чисел с
помощью алгоритма Евклида.
5. Дано натуральное число, кратное 3. Получите сумму кубов этого
числа, затем сумму кубов получившегося числа и т.д. Проверьте на
нескольких примерах, действительно ли в конечном итоге получится 153.
6. Разработайте программу вычисления an натуральной
степени n вещественного числа a за наименьшее число операций.

Указания к выполнению работы.


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

Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
 Титульный лист.
 Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
 Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
 Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
 Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
 Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
 Выводы по лабораторной работе.
 Ответы на контрольные вопросы.

Контрольные вопросы
 Приведите примеры рекурсивных объектов и явлений. Обоснуйте
проявление рекурсивности.
 Почему при правильной организации рекурсивные вызовы не
зацикливаются?
 Почему не отождествляются совпадающие идентификаторы при
многократных рекурсивных вызовах?
 Почему рекурсивные обращения завершаются в порядке,
обратном вызовам этих обращений?
 Чем ограничено при выполнении программы количество
рекурсивных вызовов?
 Какой из методов в программировании является более
эффективным – рекурсивный или итерационный?
Лабораторная работа № 8. Примеры использования рекурсивных и
итеративных алгоритмов

Цель: изучить понятия рекурсии, рекурсивные функции в


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

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

Рекурсия – это определение объекта через обращение к самому себе.


Рекурсивный алгоритм – это алгоритм, в описании которого прямо
или косвенно содержится обращение к самому себе. В технике процедурного
программирования данное понятие распространяется на функцию, которая
реализует решение отдельного блока задачи посредством вызова из своего
тела других функций, в том числе и себя самой. Если при этом на очередном
этапе работы функция организует обращение к самой себе, то
такая функция является рекурсивной.
Прямое обращение функции к самой себе предполагает, что в теле
функции содержится вызов этой же функции, но с другим
набором фактических параметров. Такой способ организации работы
называется прямой рекурсией. Например, чтобы найти сумму
первых n натуральных чисел, надо сумму первых (n-1) чисел сложить с
числом n, то есть имеет место зависимость: Sn=Sn-
+n. Вычисление происходит с помощью аналогичных рассуждений. Такая
1

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


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

Рекурсия – фундаментальное понятие в математике и компьютерных


науках. В языках программирования рекурсивной программой называется
программа, которая обращается сама к себе (подобно тому, как в математике
рекурсивная функция определяется через понятия самой этой функции).
Рекурсивная программа не может вызывать себя до бесконечности,
следовательно, вторая важная особенность рекурсивной программы –
наличие условия завершения, позволяющее программе прекратить вызывать
себя.
Таким образом рекурсия в программировании может быть определена
как сведение задачи к такой же задаче, но манипулирующей более простыми
данными.
Как следствие, рекурсивная программа должна иметь как минимум два
пути выполнения, один из которых предполагает рекурсивный вызов (случай
«сложных» данных), а второй – без рекурсивного вызова (случай «простых»
данных).
Примеры рекурсивных программ. В ряде случаев рекурсивную
подпрограмму можно построить непосредственно из
формального  математического описания задачи.
 
Факториал
{ Function Fact(n:byte):longint;
begin
n! = n * (n-1)!, при n>0   if n=0
1, n=0     then Fact:=1
    else Fact:=n*Fact(n-1)
end;
Числа Фибоначчи
Function F(n:byte):longint;
begin
Фn = Фn-1 + Фn-2, при n>1   if n <= 1
{
Ф0 = 1, Ф1 = 1     then F:=1
    else F:= F(n-1)+F(n-2)
end;
 
Рекурсия и итерация. Рекурсивную программу всегда можно преобраз
овать в нерекурсивную (итеративную, использующую циклы), которая
выполняет те же вычисления. И наоборот, используя рекурсию, любое
вычисление, предполагающее использование циклов, можно реализовать, не
прибегая к циклам.
К примеру, вычисление факториала и чисел Фибоначчи можно
реализовать без рекурсии:
 
Факториал
Function Fact(n:byte):longint; Function Fact(n:byte):longint;
var F, i : byte; begin
begin   if n=0
  F:=1;     then Fact:=1
  for i:=1 to n do F:=F*i;     else Fact:=n*Fact(n-1)
  Fact:=F end;
end;
Числа Фибоначчи
Function F(n:byte):longint; Function F(n:byte):longint;
var F0, F1, F2 : longint; i : byte; begin
begin   if n <= 1
  F0:=1; F1:=1;     then F:=1
  for i:=2 to n do     else F:= F(n-1)+F(n-2)
    begin end;
      F2:=F1+F0; F0:=F1; F1:=F2;
    end;
  F:=F1
end;

При выполнении рекурсивной подпрограммы сначала происходит


«рекурсивное погружение», а затем «возврат вычисленных результатов».
Например, вычисление 5! при помощи вызова Fact(5) будет происходить
следующим образом:
Fact(5)  5 * Fact(4)                     5 * 24       120
                                              
              4* Fact(3)                   4 * 6
                                              
                3 * Fact(2)                3 * 2
                                              
                   2 * Fact(1)             2 * 1
                                              
                         1                   1 

Как видно на примере вычисления 5!, при «рекурсивном погружении»


функция Fact вызывает точно такой же новый экземпляр самой себя. При
этом сама функция как бы еще не завершилась, а новый ее экземпляр уже
начинает работать. И только когда новый экземпляр завершит работу
(«вернет вычисленные результаты»), будет продолжена работа самой
функции.

Стек. Информация о таких незавершенных вызовах рекурсивных


подпрограмм (а это, в самом простом представлении, значения переменных,
необходимых для работы подпрограммы) запоминается в специальной
области памяти – стеке. При работе в среде Turbo Pascal содержимое стека и
весть процесс появления и завершения рекурсивных вызовов можно
просмотреть в окне «Call Stack», которое вызывается нажатием клавиш
Ctrl+F3.
Размер стека по умолчанию – 16Кб. Если незавершенных рекурсивных
вызов слишком много (хотя вы по ошибке могли написать и программу с
бесконечной рекурсией – тогда вам ничто не поможет), то система выдаст
сообщение «Stack overflow» («Переполнение стека»). В этом случае есть
смысл увеличить размер стека используя меню «Options – Memory Sizes ”.
Также помните, что нажав клавиши Ctrl+O O, вы перед текстом
программы получите список значений всех директив компилятора Turbo
Pascal, в котором можно изменять признаки их активности и значения,
установленные по умолчанию.
{$A+,B-,D+,E-,F-,G+,I-,L+,N+,O-,P-,Q-,R-,S-,T-,V+,X+}
{$M 16384,0,655360}
Размер стека определяется первым параметром директивы $M. Второй
и третий ее параметры – нижняя и верхняя границы области динамически
выделяемой памяти (кучи, heep). Размер стека можно увеличить
непосредственно в такой строке, однако эти установки будут действовать
только для данной программы.
Сложность рекурсивных вычислений. При относительной простоте
написания, у рекурсивных подпрограмм часто встречается существенный
недостаток – неэффективность. Так, сравнивая скорость вычисления чисел
Фибоначчи с помощью итеративной и рекурсивной функции можно
заметить, что итеративная функция выполняется почти «мгновенно», не
зависимо от значения n. При использовании же рекурсивной функции уже
при n=40 заметна задержка при вычислении, а при больших n результат
появляется весьма не скоро.
Неэффективность рекурсии проявляется в том, что одни и те же
вычисления производятся по многу раз. Так для вычисления 40-го числа
Фибоначчи схема рекурсивных вызовов представлена на рисунке ниже.

Оценить сложность рекурсивных вычислений (количество


рекурсивных вызовов) можно с помощью рекуррентных
соотношений. Рекуррентное соотношение – это рекурсивная функция с
целочисленными значениями. Значение любой такой функции можно
определить, вычисляя все ее значения начиная с наименьшего, используя на
каждом шаге ранее вычисленные значения для подсчета текущего значения.
Рекуррентные выражения используются, в частности, для
определения сложности рекурсивных вычислений.
Например, пусть мы пытаемся вычислить числа Фибоначчи по
рекурсивной схеме
F(i) = F(i-1) + F(i-2), при N >= 1; F(0) = 1; F(1) = 1;
с помощью указанной выше рекурсивной подпрограммы-функции F(n).
Требующееся при вычислении значения F(N) по такой схеме
количество рекурсивных вызовов может быть получено из решения
рекуррентного выражения
TN = TN-1 + TN-2, при N >= 1; T0 = 1; T1 = 1
TN приблизительно равно ФN , где Ф »1.618 - золотая пропорция
(«золотое сечение»), т.е. приведенная выше программа потребует
экспоненциальных временных затрат на вычисления.
Метод «разделяй и властвуй». Многие алгоритмы используют два
рекурсивных вызова, каждый из которых работает приблизительно с
половиной входных данных. Такая рекурсивная схема, по-видимому,
представляет собой наиболее важный случай хорошо известного метода
«разделяй и властвуй» (divide and conquer) разработки алгоритмов.
В качестве примера рассмотрим задачу отыскания максимального из N
элементов, сохраненных в массиве a[1], . . . , a[N] с элементами типа Item. Эта
задача легко может быть решена за один проход массива:
Max:=a[1];
For i:=1 to N do
   if a[i] > Max then Max:=a[i];

Рекурсивное решение типа «разделяй и властвуй» - еще один простой


(хотя совершенно иной) способ решения той же задачи:
Function Max (a : array of Item; l, r : integer) : Item;
var u, v : Item; m : integer;
begin
   m := (l+r) / 2;
   if (l = r)
      then Max := a[l]
      else begin
              u := Max (a, l, m);
              v := Max (a, m+1, r);
              if (u > v) then Max := u else Max := v
           end
end;
Чаще всего подход “разделяй и властвуй» используют из-за того, что
он обеспечивает более быстрые решения, чем итерационные алгоритмы.
Основным недостатком алгоритмов типа «разделяй и властвуй»
является то, что делят задачи на независимые подзадачи. Когда подзадачи
независимы, это часто приводит к недопустимо большим затратам времени,
так как одни и те же подзадачи начинают решаться по многу раз.
К примеру, приведенная выше рекурсивная схема вычисления чисел
Фибоначчи абсолютно недопустима при больших значениях N, так как
приводит к многократным повторным вычислениям, и экспоненциальной
сложности вычислений (TN приблизительно равно ФN , где Ф »1.618 есть
золотая пропорция).
Обходить подобные ситуации позволяет подход, известный как
динамическое программирование.
Динамическое программирование. Общий подход для реализации
рекурсивных программ, который дает возможность получать эффективные и
элегантные решения для обширного класса задач.
Технология, называемая восходящим динамическим
программированием (bottom-up dynamic programming) основана на том, что
значение рекурсивной функции можно определить, вычисляя все значения
этой функции, начиная с наименьшего, используя на каждом шаге ранее
вычисленные значения для подсчета текущего значения.
Она применима к любому рекурсивному вычислению при условии, что
мы можем позволить себе хранить все ранее вычисленные значения. Что в
результате позволит уменьшить временную зависимость с экспоненциальной
на линейную !
Нисходящее динамическое программирование (top-
down dynamic programming) – еще более простая технология. Она позволяет
выполнять рекурсивные функции при том же количестве итераций, что и
восходящее динамическое программирование. Технология требует введения
в рекурсивную программу неких средств, обеспечивающих сохранение
каждого вычисленного значения и проверку сохраненных значений во
избежание их повторного вычисления.
К примеру, сохраняя вычисленные значения в статическом массиве
K[1..100] (предварительно проинициализированном, к примеру, числом -1),
мы явным образом исключим любые повторные вычисления.
Приведенная ниже программа вычисляет F(N) за время,
пропорциональное N.

Function Max (a : array of Item; l, r : integer) : Item;


var u, v : Item; m : integer;
begin
   m := (l+r) / 2;
   if (l = r)
      then Max := a[l]
      else begin
              u := Max (a, l, m);
              v := Max (a, m+1, r);
              if (u > v) then Max := u else Max := v
           end
end;
 
Примеры задач, решаемых с помощью рекурсии.
 
Поиск максимального элемента массива. Поскольку текст функции
для поиска максимального элемента массива приведен  выше, остановимся
лишь на особенностях ее применения.
Type Item = integer;
Const Mas : array [1..10] of Item = (3,6,2,4,1,8,0,9,5,7);

Function Max (A : array of Item; l, r : integer) : Item;


. . . . .
end;

begin
  writeln (Max(Mas,0,9))
{l=0, r=9 т.к. параметр A в списке формальных параметров
функции Max – открытый массив, т.е. значения индексов начинаются
от 0}
end

Поиск элемента в упорядоченном массиве (двоичный поиск).


Имеется упорядоченный массив и эталонный элемент. Требуется
определить, содержится ли эталон в массиве. Если «да», то вернуть
соответствующий номер позиции. Если «нет» - вывести сообщение.
Для решения задачи используется метод деления исходного массива
пополам.
С эталоном сравнивается «средний» (расположенный по середине)
элемент массива. Если он меньше эталона – поиск продолжается в правой
половине массива. Если меньше – в левой.
Поиск ведется до тех пор, пока не будет обнаружено соответствие, или
пока длина участков массива, в которых ведется поиск, не станет меньше 1.
Type Item = integer;
Const A : array[1..10] of Item = (1,2,3,4,5,6,7,8,9,10);

Function Se (a : array of Item; e : Item; l, r : integer) : integer;
{a – массив, е – эталон поиска,
l, r – левая и правая границы подмассива, в котором производится поиск
Функция возвращает позицию найденного элемента (нумерация от 0) или -1 }
var m : integer;
begin
  if (r=l) and (a[r]<>e) then Se:=-1
    else begin
           m := (l+r) div 2;
           if (e = a[m])
             then Se := m
             else if e < a[m]
                    then Se := Se (a, e, l, m)
                    else Se := Se (a, e, m+1, r)
end;

begin
  P:= Se(A,0,0,9);
  if P<>-1 then writeln(‘Искомый элемент в позиции ’, P+1)
           else writeln(‘Искомый элемент не присутствует в массиве’)
end.

Ханойские башни. Имеется три стержня A, B, C. На стержне А надето


N колец, причем кольцо меньшего диаметра должно располагаться над
кольцом большего диаметра. Требуется переместить N колец с А на С,
используя В как промежуточный стержень. При перемещении колец
кольцо меньшего диаметра можно класть поверх кольца большего диаметра,
но не наоборот. Программа должна распечатать протокол перемещений
(цепочку команд вида Х -> Y).
Например, в случае N=3 исходную и конечную ситуацию можно
изобразить так:

Протокол действий будет иметь вид:


A  C, A  B, C  B,          (2)
A  C,                          (3)
B  A, B  C, A  C  (4)

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


действий, записанных в каждой из строк, и проиллюстрируем их:
(1)   – Переместить N колец с А на С, используя В как промежуточный
(2)   – Переместить (N-1) кольцо с А на В, используя С как
промежуточный
(3)   – Переместить кольцо с А на С
(4)   – Переместить (N-1) кольцо с В на С, используя А как
промежуточный
К тому же (1) = (2)  (3)  (4). Т.е. исходная задача (1), работающая
с N кольцами, сводится рекурсивно к таким же по сути задачам (2) и (4), в
которых используется (N - 1) кольцо.
Программная реализация решения:
Procedure Towers(n:byte; A, C, B : char)
{переложить N колец с А на С, используя В как промежуточный}
begin
  if n=1 then writeln(A, ‘  ’, C)
         else begin
                Towers(n-1, A, B, C);
                writeln(A, ‘  ‘, C);
                Towers(n-1, B, C, A)
              end
end;
begin
  Towers(4, ‘A’, ‘C’, ‘B’) { решаем задачу при N = 4 }
end.
 
Размен денег. Имеется некоторая сумма денег S и набор монет с
номиналами a1, …, an. 
Монета каждого номинала имеется в единственном экземпляре.
Необходимо найти все возможные способы разменять сумму S при помощи
этих монет.
const n = 6;  {число монет}
      s = 33; {требуемая сумма}
      a : array[1..n]of integer =(1,2,3,5,10,15); {номиналы монет}
var kol : array[1..n] of integer; {сколько взято таких монет -0 или
1}
    found : boolean; { признак, что хотя бы одно решение найдено}

procedure rec(k,s1:integer);
{k – число уже использовавшихся монет, s1 – набранная сумма}
var i : integer;
  begin
    if s=s1 then begin  {вывод найденного решения}
      found:=true;
      for i:=1 to n do
        if kol[i]>0 then write(a[i],' ');
      writeln; exit
    end;
    for i:=k+1 to n do  {цикл по всем еще неиспользовавшимся монетам}
      begin
        {самое критичное место рекурсивного перебора –
         «взять-проверить-вернуть обратно, чтобы взять следующую»}
        kol[i]:=1;      {взять i-ю монету}
        rec(i,s1+a[i]); {проверить, годится ли}
        kol[i]:=0;      {вернуть i-ю обратно, чтобы взять следующую}
      end;
  end;
begin
  found:=false;
  rec(0,0);
  if not found then writeln(‘Разменять невозможно’)
end.
 
Размен денег 2. К условию предыдущей задачи добавим, что монет
каждого номинала может быть несколько.
В этом случае изменится только перебор очередных монет
const n = 6;  {число монет}
      s = 33; {требуемая сумма}
      a : array[1..n]of integer = (1,2,3,5,10,15); {номиналы монет}
      b : array[1..n]of integer = (1,1,5,1,3,1); {количество монет}
var kol   : array[1..n] of integer; {сколько взято таких монет}
    found : boolean; { признак, что хотя бы одно решение найдено}

procedure rec(k,s1:integer);
{k – число уже использовавшихся номиналов, s1 – набранная сумма}
var i, p : integer;
  begin
    if s=s1 then begin {вывод найденного решения}
      found:=true;
      for i:=1 to n do
        if kol[i]>0 then write(a[i] ,':',kol[i],' ');
      writeln; exit
    end;
    for i:=k+1 to n do  {цикл по всем еще неиспользовавшимся монетам}
      begin
        {самое критичное место рекурсивного перебора –
         «взять-проверить-вернуть обратно, чтобы взять следующие»}
         p:=b[i];         {запомнить количество i-х монет}
         while (b[i]>0) do
         begin
           inc(kol[i]);   {взять следующую i-ю монету}
           dec(b[i]);     {уменьшить число оставшихся i-х монет}
           rec(i,s1+a[i]*kol[i]);
         end;
         b[i]:=p;         {восстановить количество i-х монет}
         kol[i]:=0;       {вернуть все i-е монеты}
      end;
  end;

begin
  found:=false;
  rec(0,0);
  if not found then writeln(‘Разменять невозможно’)
end.
 
Анализ трудоемкости рекурсивных алгоритмов методом подсчета
вершин дерева рекурсии
Рекурсивные алгоритмы относятся к классу алгоритмов с высокой
ресурсоемкостью, так как при большом количестве самовызовов
рекурсивных функций происходит быстрое заполнение стековой области.
Кроме того, организация хранения и закрытия очередного слоя рекурсивного
стека являются дополнительными операциями, требующими временных
затрат. На трудоемкость рекурсивных алгоритмов влияет и количество
передаваемых функцией параметров.
Рассмотрим один из методов анализа трудоемкости рекурсивного
алгоритма, который строится на основе подсчета вершин рекурсивного
дерева. Для оценки трудоемкости рекурсивных алгоритмов строится полное
дерево рекурсии. Оно представляет собой граф, вершинами которого
являются наборы фактических параметров при всех вызовах функции,
начиная с первого обращения к ней, а ребрами – пары таких наборов,
соответствующих взаимным вызовам. При этом вершины дерева рекурсии
соответствуют фактическим вызовам рекурсивных функций. Следует
заметить, что одни и те же наборы параметров могут соответствовать разным
вершинам дерева. Корень полного дерева рекурсивных вызовов –
это вершина полного дерева рекурсии, соответствующая начальному
обращению к функции.
Важной характеристикой рекурсивного алгоритма является глубина
рекурсивных вызовов – наибольшее одновременное количество
рекурсивных обращений функции, определяющее максимальное количество
слоев рекурсивного стека, в котором осуществляется хранение отложенных
вычислений. Количество элементов полных рекурсивных обращений всегда
не меньше глубины рекурсивных вызовов. При разработке рекурсивных
программ необходимо учитывать, что глубина рекурсивных вызовов не
должна превосходить максимального размера стека используемой
вычислительной среды.
При этом объем рекурсии - это одна из характеристик
сложности рекурсивных вычислений для конкретного набора параметров,
представляющая собой количество вершин полного рекурсивного дерева без
единицы.
Будем использовать следующие обозначения для конкретного входного
параметра D:
R(D) – общее число вершин дерева рекурсии,
RV(D) – объем рекурсии без листьев (внутренние вершины),
RL(D) – количество листьев дерева рекурсии,
HR(D) – глубина рекурсии.
Например, для вычисления n -го члена последовательности Фибоначчи
разработана следующая рекурсивная функция:
int Fib(int n){ //n – номер члена последовательности
if(n<3) return 1; //база рекурсии
return Fib(n-1)+Fib(n-2); //декомпозиция
}
Тогда полное дерево рекурсии для вычисления пятого члена
последовательности Фибоначчи будет иметь вид ( рис. 1):
Рис. 1. Полное дерево рекурсии для пятого члена последовательности
Фибоначчи
Характеристиками рассматриваемого метода оценки алгоритма будут
следующие величины.
D=5 D=n
R(D)=9 R(D)=2fn-1
RV(D)=4 RV(D)=fn-1
RL(D)=5 RL(D)=fn
HR(D)=4 HR(D)=n-1

Пример 1. Задача о разрезании прямоугольника на квадраты.


Дан прямоугольник, стороны которого выражены натуральными
числами. Разрежьте его на минимальное число квадратов с натуральными
сторонами. Найдите число получившихся квадратов.
Разработаем рекурсивную триаду.
Параметризация: m, n – натуральные числа, соответствующие размерам
прямоугольника.
База рекурсии: для m=n число получившихся квадратов равно 1, так как
данный прямоугольник уже является квадратом.

Декомпозиция: если  , то возможны два случая m < n или m > n.


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

Рис. 2. Пример разрезания прямоугольника 13x5 на квадраты


#include "stdafx.h"
#include <iostream>
using namespace std;
int kv(int m,int n);

int _tmain(int argc, _TCHAR* argv[]) {


int a,b,k;
printf("Введите стороны прямоугольника->");
scanf("%d%d",&a,&b);
k = kv(a,b);
printf("Прямоугольник со сторонами %d и %d можно разрезать
на %d квадратов",a,b,k);
system("pause");
return 0;
}

int kv(int m,int n){ //m,n – стороны прямоугольника


if(m==n) return 1; //база рекурсии
if(m>n) return 1+kv(m-n,n); //декомпозиция для m>n
return 1+kv(m,n-m); //декомпозиция для m<n
}
Характеристиками рассматриваемого метода оценки алгоритма будут
следующие величины ( рис. 3).
D = (13, 5) D = (m, n), m   n, худший случай
R(D)=6 R(D)=m
RV(D)=4 RV(D)=m-2
RL(D)=1 RL(D)=1
HR(D)=6 HR(D)=m

Рис. 3. Пример полного дерева рекурсии для разрезания прямоугольника


13x5 на квадраты

Пример 2. Задача о нахождении центра тяжести выпуклого


многоугольника.
Выпуклый многоугольник задан на плоскости координатами своих
вершин. Найдите его центр тяжести.
Разработаем рекурсивную триаду.
Параметризация: x, y – вещественные массивы, в которых
хранятся координаты вершин многоугольника; n – это число вершин
многоугольника, по условию задачи, n>1 так как минимальное число вершин
имеет двуугольник (отрезок).
База рекурсии: для n=2 в качестве многоугольника
рассматривается отрезок, центром тяжести которого является его середина
(рис. 4А). При этом середина делит отрезок в отношении 1 : 1.
Если координаты концов отрезка заданы как (x0,y0) и (x1,y1),
то координаты середины вычисляются по формуле:
Декомпозиция: если n>2, то рассмотрим последовательное нахождение
центров тяжести треугольника, четырехугольника и т.д.
Для n=3 центром тяжести треугольника является точка пересечения его
медиан, которая делит каждую медиану в отношении 2 : 1, считая от
вершины. Но основание медианы – это середина отрезка, являющегося
стороной треугольника. Таким образом, для нахождения центра тяжести
треугольника необходимо: найти центр тяжести стороны треугольника
(отрезка), затем разделить в отношении 2 : 1, считая от вершины, отрезок,
образованный основанием медианы и третьей вершиной (рис. 4B).
Для n=4 центром тяжести четырехугольника является точка, делящая в
отношении 3 : 1, считая от вершины, отрезок: он образован центром тяжести
треугольника, построенного на трех вершинах, и четвертой вершиной (рис.
4C).

Рис. 4. Примеры построения центров тяжести многоугольников


Таким образом, для нахождения центра тяжести n -угольника
необходимо разделить в отношении (n-1): 1, считая от вершины, отрезок: он
образован центром тяжести (n-1) -угольника и n -ой вершиной
рассматриваемого многоугольника. Если концы отрезка заданы
координатами вершины (xn,yn) и центра тяжести (n-1) -угольника (cxn-1,cyn-1),
то при делении отрезка в данном отношении получаем координаты:

#include "stdafx.h"
#include <iostream>
using namespace std;
#define max 20
void centr(int n,float *x, float *y, float *c);

int _tmain(int argc, _TCHAR* argv[]){


int m, i=0;
FILE *f;
if ( ( f = fopen("in.txt", "r") ) == NULL )
perror("in.txt");
else {
fscanf(f, "%d",&m);
printf("\n%d",m);
if ( m < 2 || m > max ) //вырожденный многоугольник
printf ("Вырожденный многоугольник");
else {
float *px,*py,*pc;
px = new float[m];
py = new float[m];
pc = new float[2];
pc[0] = pc[1] = 0;
while(i<m) {
fscanf(f, "%f %f",&px[i], &py[i]);
printf("\n%f %f",px[i], py[i]);
i++;
}
centr(m,px,py,pc);
printf ("\nЦентр тяжести имеет координаты:
(%.4f, %.4f)",pc[0],pc[1]);
delete [] pc;
delete [] py;
delete [] px;
}
fclose(f);
}
system("pause");
return 0;
}

void centr(int n,float *x, float *y, float *c){


//n - количество вершин,
//x,y - координаты вершин,
//c - координаты центра тяжести
if(n==2){ //база рекурсии
c[0]=(x[0]+x[1])/2;
c[1]=(y[0]+y[1])/2;
}
if(n>2) { //декомпозиция
centr(n-1,x,y,c);
c[0]= (x[n-1] + (n-1)*c[0])/n;
c[1]= (y[n-1] + (n-1)*c[1])/n;
}
}
Характеристиками рассматриваемого метода оценки алгоритма будут
следующие величины.
D=4 D=n
R(D)=3 R(D)=n-1
RV(D)=1 RV(D)=n-3
RL(D)=1 RL(D)=1
HR(D)=3 HR(D)=n-1
Однако в данном случае для более достоверной оценки необходимо
учитывать емкостные характеристики алгоритма.

Пример 3. Задача о разбиении целого на части.


Найдите количество разбиений натурального числа на сумму
натуральных слагаемых.
Разбиение подразумевает представление натурального числа в виде
суммы натуральных слагаемых, при этом суммы должны отличаться набором
чисел, а не их последовательностью. В разбиение также может входить одно
число.
Например, разбиение числа 6 будет представлено 11 комбинациями:
6
5+1
4+2, 4+1+1
3+3, 3+2+1, 3+1+1+1
2+2+2, 2+2+1+1, 2+1+1+1+1
1+1+1+1+1+1
Рассмотрим решение в общем виде. Пусть зависимость R(n,k) вычисляет
количество разбиений числа n на сумму слагаемых, не превосходящих k.
Опишем свойства R(n,k).
Если в сумме все слагаемые не превосходят 1, то
такое представление единственно, то есть R(n,k)=1.
Если рассматриваемое число равно 1, то при любом натуральном
значении второго параметра разбиение также единственно: R(n,k)=1.
Если второй параметр превосходит значение первого, то
имеет место равенство R(n,k)=R(n,n), так как для
представления натурального числа в сумму не могут входить числа,
превосходящие его.
Если в сумму входит слагаемое, равное первому параметру, то
такое представление также единственно (содержит только это слагаемое),
поэтому имеет место равенство: R(n,n)=R(n,n-1)+1.
Осталось рассмотреть случай (n>k). Разобьем все представления
числа n на непересекающиеся разложения: в одни обязательно будет входить
слагаемое k, а другие суммы не содержат k. Первая группа сумм,
содержащая k, эквивалентна зависимости R(n-k,k), что следует после
вычитания числа k из каждой суммы. Вторая группа сумм
содержит разбиение числа n на слагаемые, каждое из которых не
превосходит k-1, то есть число таких представлений равно R(n,k-1). Так как
обе группы сумм не пересекаются, то R(n,k)=R(n-k,k)+R(n,k-1).
Разработаем рекурсивную триаду.
Параметризация: Рассмотрим разбиение натурального числа n на сумму
таких слагаемых, которые не превосходят натурального числа k.
База рекурсии: исходя из свойств рассмотренной зависимости,
выделяются два базовых случая:
при n=1     R(n,k)=1,
при k=1     R(n,k)=1.
Декомпозиция: общий случай задачи сводится к трем случаям, которые и
составляют декомпозиционные отношения.
при n=k     R(n,k)=R(n,n-1)+1,
при n<k     R(n,k)=R(n,n),
при n>k     R(n,k)=R(n-k,k)+R(n,k-1).
#include "stdafx.h"
#include <iostream>
using namespace std;
unsigned long int Razbienie(unsigned long int n,
unsigned long int k);
int _tmain(int argc, _TCHAR* argv[]){
unsigned long int number, max,num;
printf ("\nВведите натуральное число: ");
scanf ("%d", &number);
printf ("Введите максимальное натуральное слагаемое в
сумме: ");
scanf ("%d", &max);
num=Razbienie(number,max);
printf ("Число %d можно представить в виде суммы с
максимальным слагаемым %d.", number, max);
printf ("\nКоличество разбиений равно %d",num);
system("pause");
return 0;
}

unsigned long int Razbienie(unsigned long int n,


unsigned long int k){
if(n==1 || k==1) return 1;
if(n<=k) return Razbienie(n,n-1)+1;
return Razbienie(n,k-1)+Razbienie(n-k,k);
}

Пример 4. Задача о переводе натурального числа в


шестнадцатеричную систему счисления.
Дано натуральное число, не выходящее за пределы типа unsigned long.
Число представлено в десятичной системе счисления. Переведите его в
систему счисления с основанием 16.
Пусть требуется перевести целое число n из десятичной в р -ичную
систему счисления (по условию задачи, р = 16), то есть найти такое k, чтобы
выполнялось равенство n10=kp.
Параметризация: n – данное натуральное число, р – основание системы
счисления.
База рекурсии: на основании правил перевода чисел из десятичной
системы в систему счисления с основанием р, деление нацело
на основание системы выполняется до тех пор, пока неполное частное не
станет равным нулю, то есть: если целая часть частного n и р равна нулю,
то k = n. Данное условие можно реализовать иначе, сравнив n и р: целая часть
частного равна нулю, если n < р.
Декомпозиция: в общем случае k формируется из цифр целой части
частного n и р, представленной в системе счисления с основанием р, и
остатка от деления n на p.
#include "stdafx.h"
#include <iostream>
using namespace std;
#define maxline 50
void perevod( unsigned long n, unsigned int p,FILE *pf);

int _tmain(int argc, _TCHAR* argv[]){


unsigned long number10;
unsigned int osn=16;
char number16[maxline];
FILE *f;
if ((f=fopen("out.txt", "w"))==NULL)
perror("out.txt");
else {
printf ("\nВведите число в десятичной системе: ");
scanf("%ld", &number10);
perevod(number10, osn, f);
fclose(f);
}
if ((f=fopen("out.txt", "r"))==NULL)
perror("out.txt");
else {
fscanf(f,"%s",number16);
printf("\n %ld(10)=%s(16)", number10, number16);
fclose(f);
}
system("pause");
return 0;
}

void perevod(unsigned long n, unsigned int p, FILE *pf){


char c;
unsigned int r;
if(n >= p) perevod (n/p, p, pf);//декомпозиция
r=n%p;
c=r < 10 ? char (r+48) : char (r+55);
putc(c, pf);
}

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

Краткие итоги
1. Рекурсия характеризуется определением объекта посредством
ссылки на себя.
2. Рекурсивные алгоритмы содержат в своем теле прямое или
опосредованное обращение с самим себе.
3. Рекурсивные функции содержат в своем теле обращение к самим
себе с измененным набором параметров в виде прямой рекурсии. При этом
обращение к себе может быть организовано посредством косвенной
рекурсии – через цепочку взаимных обращений функций, замыкающихся в
итоге на первоначальную функцию.
4. Решение задач рекурсивными способами проводится
посредством разработки рекурсивной триады.
5. Целесообразность применения рекурсии в программировании
обусловлена спецификой задач, в постановке которых явно или опосредовано
указывается на возможность сведения задачи к подзадачам, аналогичным
самой задаче.
6. Рекурсивные методы решения задач широко используются при
моделировании задач из различных предметных областей.
7. Рекурсивные алгоритмы относятся к ресурсоемким алгоритмам.
Для оценки сложности рекурсивных алгоритмов учитывается число вершин
полного рекурсивного дерева, количество передаваемых параметров,
временные затраты на организацию стековых слоев.

Задание на лабораторную работу

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


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

Выполните приведенные ниже задания.


1. Разработайте рекурсивную функцию, подсчитывающую
количество способов разбиения выпуклого многоугольника на треугольники
непересекающимися диагоналями.
2. В Фибоначчиевой системе счисления числа формируются по
правилам.
 Используются только символы 0 и 1;
 Каждый разряд соответствует элементу последовательности
Фибоначчи 1, 2, 3, 5, 8, …, то есть указывает на наличие или
отсутствие такового;
 В соседних разрядах не могут стоять символы 1, так как это
автоматически означает формирование следующего за ними
разряда. Например, 1710 = 1310 + 310 + 110 = 100101ф.
Составьте программу перевода числа из десятичной системы в
Фибоначчиевую. Считать входные данные введенными корректно.
3. Найдите походящие дроби рационального числа x/y ( x –

неотрицательно, y – положительно). Например,  , то есть для х =


5, y = 6 ответом будет последовательность [0; 1, 5].
4. Вычислите определитель квадратной матрицы размера nxn.

Указания к выполнению работы.


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

Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
 Титульный лист.
 Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
 Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
 Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
 Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
 Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
 Выводы по лабораторной работе.
 Ответы на контрольные вопросы.

Контрольные вопросы
 Приведите примеры рекурсивных объектов и явлений. Обоснуйте
проявление рекурсивности.
 Почему при правильной организации рекурсивные вызовы не
зацикливаются?
 Почему не отождествляются совпадающие идентификаторы при
многократных рекурсивных вызовах?
 Почему рекурсивные обращения завершаются в порядке,
обратном вызовам этих обращений?
 Чем ограничено при выполнении программы количество
рекурсивных вызовов?
 Какой из методов в программировании является более
эффективным – рекурсивный или итерационный?
 Можно ли случай косвенной рекурсии свести к прямой рекурсии?
Ответ обоснуйте.
 Может ли рекурсивная база содержать несколько тривиальных
случаев? Ответ обоснуйте.
 Являются ли параметры, база и декомпозиция единственными для
конкретной задачи? Ответ обоснуйте.
 С какой целью в задачах происходит пересмотр или
корректировка выбранных параметров, выделенной базы или
случая декомпозиции?
 Является ли рекурсия универсальным способом решения задач?
Ответ обоснуйте.
 Почему для оценки трудоемкости рекурсивного алгоритма
недостаточно одного метода подсчета вершин рекурсивного дерева?
Лабораторная работа № 9. Ознакомление STL компонентами и
контейнерами. Итераторы и состав стандартной библиотеки шаблонов

Цель: ознакомится с STL компонентами и контейнерами, изучить


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

Теоретическая часть
Стандартная библиотека шаблонов (STL)
Стандартная библиотека шаблонов (сокр. «STL» от «Standard
Template  Library») — это часть Стандартной библиотеки С++, которая
содержит набор шаблонов контейнерных классов
(например, std::vector и std::array), алгоритмов и итераторов. Изначально
она была сторонней разработкой, но позже была включена в Стандартную
библиотеку С++. Если вам нужен какой-нибудь общий класс или
алгоритм, то, скорее всего, в Стандартной библиотеке шаблонов он уже
есть. Положительным моментом является то, что вы можете использовать
эти классы без необходимости писать и отлаживать их самостоятельно (и
разбираться в том, как они реализованы). Кроме того, вы получаете
достаточно эффективные (и уже много раз протестированные) версии этих
классов. Недостатком является то, что не всё так просто/очевидно с
функционалом Стандартной библиотеки шаблонов и это может быть
несколько непонятно новичку, так как большинство классов на самом деле
являются шаблонами классов.
К счастью, вы можете отделить себе кусочек Стандартной
библиотеки шаблонов, чтобы его «распробовать» и при этом игнорировать
всё остальное до тех пор, пока вы в нем не разберетесь.
На следующих нескольких уроках мы рассмотрим типы
контейнерных классов, алгоритмов и итераторов, которые предоставляет
Стандартная библиотека шаблонов. Затем мы еще углубимся в изучение
некоторых конкретных классов.
Контейнеры STL
Безусловно, наиболее часто используемым
функционалом библиотеки STL являются контейнерные классы (или
как их еще называют — «контейнеры»). Библиотека STL содержит много
разных контейнерных классов, которые можно использовать в разных
ситуациях. Если говорить в общем, то контейнеры STL делятся на три
основные категории:
 последовательные;
 ассоциативные;
 адаптеры.
Сейчас сделаем их краткий обзор.
Последовательные контейнеры
Последовательные контейнеры (или «контейнеры
последовательности») — это контейнерные классы, элементы которых
находятся в последовательности. Их определяющей характеристикой
является то, что вы можете вставить свой элемент в любое место
контейнера. Наиболее распространенным примером последовательного
контейнера является массив: при вставке 4-х элементов в массив, эти
элементы будут находиться (в массиве) в точно таком же порядке, в
котором вы их вставляли.
Начиная с C++11, STL содержит 6 контейнеров
последовательности:
std::vector;
std::deque;
std::array;
std::list;
std::forward_list;
std::basic_string.
Класс vector (или просто «вектор») — это динамический массив,
способный увеличиваться по мере необходимости для содержания всех
своих элементов. Класс vector обеспечивает произвольный доступ к своим
элементам через оператор индексации [], а также поддерживает вставку
и удаление элементов.
В следующей программе мы вставляем 5 целых чисел в вектор и с
помощью перегруженного оператора индексации [] получаем к ним
доступ для их последующего вывода:

Результат выполнения программы:


10 9 8 7 6
Класс deque (или просто «дек») — это двусторонняя очередь,
реализованная в виде динамического массива, который может расти с
обоих концов. Например:

Результат выполнения программы:


7 8 9 10 0 1 2 3
List (или просто «список») — это двусвязный список, каждый
элемент которого содержит 2 указателя: один указывает на следующий
элемент списка, а другой — на предыдущий элемент списка. list
предоставляет доступ только к началу и к концу списка — произвольный
доступ запрещен. Если вы хотите найти значение где-то в середине, то вы
должны начать с одного конца и перебирать каждый элемент списка до
тех пор, пока не найдете то, что ищите. Преимуществом двусвязного
списка является то, что вставка элементов происходит очень быстро, если
вы, конечно, знаете, куда хотите вставлять. Обычно для перебора
элементов двусвязного списка используются итераторы.
Хотя о классе string (и wstring) обычно не говорят, как о
последовательном контейнере, но он, по сути, таковым является,
поскольку его можно рассматривать как вектор с элементами типа char
(или wchar).
Ассоциативные контейнеры
Ассоциативные контейнеры — это контейнерные классы, которые
автоматически сортируют все свои элементы (в том числе и те, которые
вставляете вы). По умолчанию ассоциативные контейнеры выполняют
сортировку элементов, используя оператор сравнения <.
set — это контейнер, в котором хранятся только уникальные
элементы, и повторения запрещены. Элементы сортируются в
соответствии с их значениями.
multiset — это set, но в котором допускаются повторяющиеся
элементы.
map (или «ассоциативный массив») — это set, в котором каждый
элемент является парой «ключ-значение». «Ключ» используется для
сортировки и индексации данных и должен быть уникальным. А
«значение» — это фактические данные.
multimap (или «словарь») — это map, который допускает
дублирование ключей. Все ключи отсортированы в порядке возрастания, и
вы можете посмотреть значение по ключу.
Адаптеры
Адаптеры — это специальные предопределенные контейнерные
классы, которые адаптированы для выполнения конкретных заданий.
Самое интересное заключается в том, что вы сами можете выбрать, какой
последовательный контейнер должен использовать адаптер.
stack (стек) — это контейнерный класс, элементы которого
работают по принципу LIFO (англ. «Last  In,  First  Out» = «Последним
Пришёл, Первым Ушёл»), т.е. элементы вставляются (вталкиваются) в
конец контейнера и удаляются (выталкиваются) оттуда же (из конца
контейнера). Обычно в стеках используется deque в качестве
последовательного контейнера по умолчанию (что немного странно,
поскольку vector был бы более подходящим вариантом), но вы также
можете использовать vector или list.
queue (очередь) — это контейнерный класс, элементы которого
работают по принципу FIFO (англ. «First  In,  First  Out» = «Первым
Пришёл, Первым Ушёл»), т.е. элементы вставляются (вталкиваются) в
конец контейнера, но удаляются (выталкиваются) из начала контейнера.
По умолчанию в очереди используется deque в качестве
последовательного контейнера, но также может использоваться и list.
priority_queue (очередь с приоритетом) — это тип очереди, в
которой все элементы отсортированы (с помощью оператора сравнения <).
При вставке элемента, он автоматически сортируется. Элемент с
наивысшим приоритетом (самый большой элемент) находится в самом
начале очереди с приоритетом, также, как и удаление элементов
выполняется с самого начала очереди с приоритетом.
Итераторы STL
Итератор — это объект, который способен перебирать
элементы контейнерного класса без необходимости пользователю знать
реализацию определенного контейнерного класса. Во многих контейнерах
(особенно в списке и в ассоциативных контейнерах) итераторы являются
основным способом доступа к элементам этих контейнеров.
Функционал итераторов
Об итераторе можно думать, как об указателе на определенный
элемент контейнерного класса с дополнительным
набором перегруженных операторов для выполнения чётко
определенных функций:
Оператор * возвращает элемент, на который в данный момент
указывает итератор.
Оператор ++ перемещает итератор к следующему элементу
контейнера. Большинство итераторов также предоставляют оператор −
− для перехода к предыдущему элементу.
Операторы == и != используются для определения того, указывают
ли два итератора на один и тот же элемент или нет. Для сравнения
значений, на которые указывают два итератора, нужно сначала
разыменовать эти итераторы, а затем использовать оператор == или
оператор !=.
Оператор = присваивает итератору новую позицию (обычно начало
или конец элементов контейнера). Чтобы присвоить значение элемента, на
который указывает итератор, другому объекту, нужно сначала
разыменовать итератор, а затем использовать оператор =.
Каждый контейнерный класс имеет 4 основных метода для работы
с оператором =:
метод begin() возвращает итератор, представляющий начало
элементов контейнера.
метод end() возвращает итератор, представляющий элемент,
который находится после последнего элемента в контейнере.
метод cbegin() возвращает константный (только для чтения)
итератор, представляющий начало элементов контейнера.
метод cend() возвращает константный (только для чтения) итератор,
представляющий элемент, который находится после последнего элемента
в контейнере.
Может показаться странным, что метод end() не указывает на
последний элемент контейнера, но это сделано в целях упрощения
использования циклов: цикл перебирает элементы до тех пор, пока
итератор не достигнет метода end(), и тогда уже всё — «Баста!».
Наконец, все контейнеры предоставляют (как минимум) два типа
итераторов:
container::iterator — итератор для чтения/записи;
container::const_iterator — итератор только для чтения.
Рассмотрим несколько примеров использования итераторов.
Итерация по вектору
Заполним вектор 5-ью числами и с помощью итераторов выведем
значения вектора:
Результат выполнения программы:
01234
Итерация по списку
Выполним всё то же самое, что выше, но уже со списком:

Результат выполнения программы:


01234
Обратите внимание, код этой программы почти идентичен
вышеприведенному коду, хотя реализация векторов и списков
значительно отличается друг от друга!
Итерация по set-у
В следующей программе мы создадим set из 5 чисел и, используя
итератор, выведем эти значения:

Результат выполнения программы:


-4 2 3 8 9
Обратите внимание, хотя заполнение set-a элементами отличается от
способа заполнения вектора и списка выше, но код, используемый для
перебора элементов set-a — идентичен.
Итерация по ассоциативному массиву
Этот пример немного сложнее. Контейнеры map и multimap
принимают пары элементов (определенные как std::pair). Мы используем
вспомогательную функцию make_pair() для создания пар. std::pair
позволяет получить доступ к элементу (паре «ключ-значение») через
первый и второй члены. В нашем ассоциативном массиве мы используем
первый член в качестве «ключа», а второй в качестве «значения»:
Результат выполнения программы:
1=spider 2=dog 3=cat 4=lion 5=chicken
Обратите внимание, насколько легко с помощью итераторов
перебирать элементы контейнеров. Вам не нужно заботиться о том, как
ассоциативный массив хранит свои данные!
Заключение
Итераторы предоставляют простой способ перебора элементов
контейнерного класса без необходимости знать реализацию
определенного контейнерного класса. В сочетании с алгоритмами STL и
методами контейнерных классов итераторы становятся еще более
мощными.
Стоит отметить еще один момент: итераторы должны быть
реализованы для каждого контейнера отдельно, поскольку итератор
должен знать реализацию контейнерного класса. Таким образом,
итераторы всегда привязаны к конкретным контейнерным классам.
Алгоритмы STL
Помимо контейнеров и итераторов, библиотека STL также
предоставляет ряд универсальных алгоритмов для работы с элементами
контейнеров. Они позволяют выполнять такие операции, как поиск,
сортировка, вставка, изменение позиции, удаление и копирование
элементов контейнера.
Алгоритмы STL
Алгоритмы STL реализованы в виде глобальных функций, которые
работают с использованием итераторов. Это означает, что каждый
алгоритм нужно реализовать всего лишь один раз, и он будет работать со
всеми контейнерами, которые предоставляют набор итераторов (включая
и ваши собственные (пользовательские) контейнерные классы). Хотя это
имеет огромный потенциал и предоставляет возможность быстро писать
сложный код, у алгоритмов также есть и «тёмная сторона» — некоторая
комбинация алгоритмов и типов контейнеров может не работать/работать
с плохой производительностью/вызывать бесконечные циклы, поэтому
следует быть осторожным.
Библиотека STL предоставляет довольно много алгоритмов. На этом
уроке мы затронем лишь некоторые из наиболее распространенных и
простых в использовании алгоритмов. Для их работы нужно
подключить заголовочный файл algorithm.
Алгоритмы min_element() и max_element()
Алгоритмы min_element() и max_element() находят минимальный и
максимальный элементы в контейнере:
Результат выполнения программы:
04
Алгоритмы find() и list::insert()
В следующем примере мы используем алгоритм find(), чтобы найти
определенное значение в списке, а затем используем функцию list::insert()
для добавления нового значения в список:

Результат выполнения программы:


017234
Алгоритмы sort() и reverse()
В следующем примере мы отсортируем весь вектор, выведем
отсортированные элементы, а затем выведем их в обратном порядке:
Результат выполнения программы:
-8 -3 3 4 5 8 12
12 8 5 4 3 -3 -8
Обратите внимание, общий алгоритм sort() не работает с вектором
— у вектора есть свой собственный метод sort(), который, в данном
случае, является более эффективным.
Заключение
Хотя это всего лишь небольшой пример алгоритмов, которые
предоставляет STL, но этого уже должно быть достаточно, чтобы вы
увидели пользу и то, насколько легко использовать алгоритмы STL в
сочетании с итераторами и контейнерами.

Задание на лабораторную работу


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

Задания к лабораторной работе.


Выполните приведенные ниже задания.
1. Написать шаблонную функцию, которая выводит на экран
содержимое контейнера сначала в прямом порядке, потом в обратном
порядке (с применением обратного итератора).
2. Написать шаблонную функцию от 2ух параметров (вектор и список).
Функция должна построить и вернуть новый вектор, полученный из
исходного заменой четных элементов вектора на нечетные элементы списка.
Длины вектора и списка могут быть различны.
Вывести на экран исходные данные и результат работы функции
Пример работы:
V: v0-v1-v2-…vn
L: l1-l2-…lm
Vnew : l1-v1-l3-v3.....
3. Написать шаблонную функцию от 2ух параметров (вектор и список).
Функция должна построить и вернуть новый вектор, полученный из
исходного заменой нечетных элементов вектора на четные элементы списка.
Длины вектора и списка могут быть различны.
Вывести на экран исходные данные и результат работы функции.
Пример работы:
V: v0-v1-v2-…vn
L: l1-l2-…lm
Vnew : v0-l2-v2-l4-v4.....
4. Написать шаблонную функцию от 2ух параметров (вектор и список).
Функция должна построить и вернуть новый список, полученный из
исходного добавлением после четных элементов списка нечетных элементов
вектора.
Длины вектора и списка могут быть различны.
Вывести на экран исходные данные и результат работы функции.
Пример работы:
V: v0-v1-v2-…vn
L: l1-l2-…lm
Lnew : l1-l2-v1-l3-l4-v3.....
5. Написать шаблонную функцию от 2ух параметров (вектор и список).
Функция должна построить и вернуть новый список, полученный из
исходного добавлением перед нечетными элементами списка нечетных
элементов вектора.
Длины вектора и списка могут быть различны.
Вывести на экран исходные данные и результат работы функции.
Пример работы:
V: v0-v1-v2-…vn
L: l1-l2-…lm
Lnew : v1-l1-l2-v3-l3-l4.....
6. Написать шаблонную функцию от 2ух параметров (вектор и список).
Функция должна построить и вернуть новый список, полученный из
исходного чередованием пар элементов списка и вектора.
Длины вектора и списка могут быть различны.
Вывести на экран исходные данные и результат работы функции.
Пример работы:
V: v0-v1-v2-…vn
L: l1-l2-…lm
Lnew : l1-l2-v0-v1-l3-l4-v2-v3.....
7. Написать шаблонную функцию от 2ух параметров (вектор и список).
Функция должна построить и вернуть новый вектор, полученный из
исходного заменой четных элементов вектора на нечетные элементы списка
(считать элементы списка с конца).
Длины вектора и списка могут быть различны.
Вывести на экран исходные данные и результат работы функции.
Пример работы:
V: v0-v1-v2-…vn
L: l1-l2-…lm
Vnew : lm-v1-lm-2-v3.....
8. Написать шаблонную функцию от 2ух параметров (вектор и список).
Функция должна построить и вернуть новый вектор, полученный из
исходного заменой нечетных элементов вектора на четные элементы списка
(считать элементы списка с его конца).
Длины вектора и списка могут быть различны.
Вывести на экран исходные данные и результат работы функции.
Пример работы:
V: v0-v1-v2-…vn
L: l1-l2-…lm
Vnew : v0-lm-1-v2-lm-3-v4.....
9. Написать шаблонную функцию от 2ух параметров (вектор и список).
Функция должна построить и вернуть новый список, полученный из
исходного добавлением после четных элементов списка нечетных элементов
вектора (считать элементы вектора с его конца).
Длины вектора и списка могут быть различны.
Вывести на экран исходные данные (в прямом порядке и в обратном – с
использованием обратных итераторов) и результат работы функции.
Пример работы:
V: v0-v1-v2-…vn
L: l1-l2-…lm
Lnew : l1-l2-vn-l3-l4-vn-2.....
10. Написать шаблонную функцию от 2ух параметров (вектор и
список). Функция должна построить и вернуть новый список, полученный из
исходного добавлением перед нечетными элементами списка нечетных
элементов вектора (считать элементы вектора с его конца).
Длины вектора и списка могут быть различны.
Вывести на экран исходные данные (в прямом порядке и в обратном – с
использованием обратных итераторов) и результат работы функции.
Пример работы:
V: v0-v1-v2-…vn
L: l1-l2-…lm
Lnew : vn-l1-l2-vn-2-l3-l4.....
11. Написать шаблонную функцию от 2ух параметров (вектор и
список). Функция должна построить и вернуть новый список, полученный из
исходного чередованием пар элементов списка и вектора. Просмотр списка
ведется с его начала, а вектора – с конца.
Длины вектора и списка могут быть различны.
Вывести на экран исходные данные (в прямом порядке и в обратном – с
использованием обратных итераторов) и результат работы функции.
Пример работы:
V: v0-v1-v2-…vn
L: l1-l2-…lm
Lnew : l1-l2-vn-vn-1-l3-l4-vn-2-vn-3.....

Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
 Титульный лист.
 Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
 Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
 Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
 Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
 Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
 Выводы по лабораторной работе.
 Ответы на контрольные вопросы.

Контрольные вопросы:
1. Стандартная библиотека шаблонов (STL)
2. Контейнеры STL
3. Последовательные контейнеры
4. Ассоциативные контейнеры
5. Адаптеры
6. Итераторы STL
7. Функционал итераторов
8. Итерация по вектору
9. Итерация по списку
10.Итерация по set-у
11.Итерация по ассоциативному массиву
12.Алгоритмы STL
13.Алгоритмы min_element() и max_element()
14.Алгоритмы find() и list::insert()
15.Алгоритмы sort() и reverse()
Лабораторная работа № 10. Пользовательские шаблоны

Цель: ознакомится с пользовательскими шаблонами, реализовать


классы, содержащие несколько объектов определенного типа данных..

Теоретическая часть

Шаблоны классов
Иногда при написании программ требуется создавать классы,
зависящие от параметров — других классов. Например, если мы создаем
класс стека, вполне логично, чтобы этот класс имел параметр — тип
элемента стека. Таким образом мы могли бы, написав один класс с
параметром, получить сразу несколько конкретных классов стека (стеки
вещественных, целых чисел, пользовательских типов и т. д.). В языке C для
этой цели использовались макроопределения с параметрами. Недостатки
такого подхода в том, что препроцессор, подставляя вместо вызова
макроопределения его текст, ничего не проверяет, что является источником
многих ошибок. Обнаружить эти ошибки непросто, поскольку программист
видит только вызов макроопределения, а не тот текст, который получается
после его разворачивания.

В C++ имеется более безопасная в плане обнаружения ошибок


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

template<список_шаблонных_параметров_через_запятую>

class имя_класса возможное_наследование

{
тело_класса

};

Шаблонные параметры бывают трех видов:

1) int имя — целое число.

2) class имя — тип (не только класс, но допустимы также и встроенные


типы)

3) template<параметры> class имя — другой шаблон.

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


умолчанию, как у обычных функций.

Например, шаблонный класс стека можно создать следующим образом:

template<class T, int s>

class Stack

T data[s];

int c;

public:

Stack() { c = 0; }

void push(T x) { if(c<s) data[c++] = x; }

T pop()

{ if(!empty()) return data[--c]; else return T(); }

bool empty() { return c==0; }

};
Теперь, чтобы создать какой-нибудь стек, нужно написать примерно
следующее:

Stack<int, 200> S;

В данном случае мы создаем стек целых чисел, максимальное


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

Шаблоны и контейнерные классы


Из урока о контейнерных классах мы узнали то, как,
используя композицию, реализовать классы, содержащие несколько
объектов определенного типа данных. В качестве примера мы
использовали класс ArrayInt:
Хотя этот класс обеспечивает простой способ создания массива
целочисленных значений, что, если нам нужно будет работать со
значениями типа double? Используя традиционные методы
программирования, мы бы создали новый класс ArrayDouble для работы
со значениями типа double:
Хотя кода много, но классы почти идентичны, меняется только тип
данных! Как вы уже могли бы догадаться, это идеальный случай для
использования шаблонов. Создание шаблона класса аналогично
созданию шаблона функции. Например, создадим шаблон класса Array:
Array.h:
Как вы можете видеть, эта версия почти идентична версии ArrayInt,
за исключением того, что мы добавили объявление параметра шаблона
класса и изменили тип данных c int на T.
Обратите внимание, мы определили функцию getLength() вне тела
класса. Это необязательно, но новички обычно спотыкаются на этом из-за
синтаксиса. Каждый метод шаблона класса, объявленный вне тела класса,
нуждается в собственном объявлении шаблона. Также обратите внимание,
что имя шаблона класса — Array<T>, а не Array (Array будет указывать на
не шаблонную версию класса Array).
Вот пример использования шаблона класса Array:

Результат:
9     9.5
8     8.5
7     7.5
6     6.5
5     5.5
4     4.5
3     3.5
2     2.5
1     1.5
0     0.5
Шаблоны классов работают точно так же, как и шаблоны функций:
компилятор копирует шаблон класса, заменяя типы параметров шаблона
класса на фактические (передаваемые) типы данных, а затем компилирует
эту копию. Если у вас есть шаблон класса, но вы его не используете, то
компилятор не будет его даже компилировать.
Шаблоны классов идеально подходят для реализации контейнерных
классов, так как очень часто таким классам приходится работать с
разными типами данных, а шаблоны позволяют это организовать в
минимальном количестве кода. Хотя синтаксис несколько уродлив, и
сообщения об ошибках иногда могут быть «объемными», шаблоны
классов действительно являются одним из лучших и наиболее полезных
свойств языка C++.
Шаблоны классов в Стандартной библиотеке С++
В стандартной библиотеке языка C++ имеются уже готовые
шаблонные классы для стека (файл stack), очереди (файл queue), а также
для следующих структур данных:
1) вектор (файл vector) — структура данных для хранения
последовательности элементов на основе массива, тип элемента —
шаблонный параметр. При этом число элементов вектора может меняться
в процессе работы программы.
Создать переменную типа вектор с вещественными элементами
можно так:
vector<double> v;
Доступ к элементам вектора осуществляется как обычно для
массивов через операцию индексации, но при этом разрешается добавить
новый элемент в конец вектора при помощи метода
push_back(новый_элемент). Получить текущее число элементов вектора
можно при помощи метода size, убрать элемент из вектора можно методом
erase (его параметр — итератор, указывающий на удаляемый элемент).
2) связный (более точно, двусвязный) список — шаблонный класс
list (файл list). Добраться до его элементов несколько сложнее (см. дальше
про итераторы). Получить число его элементов можно методом size, как и
у вектора.
Вставить элемент в заданную позицию можно методом insert (его
параметры — итератор, указывающий на элемент, перед которым
происходит вставка, а также значение вставляемого элемента). Удалить
элемент из списка можно методом erase, параметром которого служит
итератор, указывающий на удаляемый элемент. Последний метод
возвращает другой итератор, указывающий на следующий за удаляемым
элемент.

Теперь вы уже поняли, чем на самом деле является std::vector<int>?


Правильно, std::vector — это шаблон класса, а int — это всего лишь
передаваемый тип данных! Стандартная библиотека С++ полна
предопределенных шаблонов классов, доступных для вашего
использования.

Шаблонные классы позволяют также иметь некоторые частные


случаи шаблонов, отличающиеся от общей реализации. Например, в
стандартной библиотеке есть частный случай шаблонного класса vector, а
именно vector<bool>.
Эта реализация шаблона vector отличается от общего случая, и дает
не массив значений типа bool, а битовый вектор, т. е. vector, каждый
элемент которого занимает один бит.
Реализация такого рода частных случаев выглядит так:
template<>
class имя<значения_параметров>
{
тело
};
Можно рассматривать и более сложные конструкции для выделения
частных случаев; например, если X — шаблон класса с двумя
шаблонными параметрами, можно выделить в отдельный частный случай
ситуацию, когда эти параметры совпадают:

template<class U>
class X<U, U>
{
тело
};
Имея набор параметров шаблона, при помощи конструкции
template class имя<параметры>;
можно заставить компилятор построить соответствующий экземпляр
шаблонного класса. Однако, если это необходимо сделать в нескольких
файлах, время компиляции может сильно увеличиться. Чтобы этого не
происходило, во всех файлах, кроме одного, нужно заменить эту
конструкцию на
extern template class имя<параметры>;
Тоже самое верно и в отношении шаблонных функций, просто class
имя <параметры> заменяется на тип имя <шаблонные_параметры>
(типы_параметров_функции) — заголовок функции с явным указанием
значений шаблонных параметров.
Как мы видели, обсуждая шаблонные функции, наибольшую пользу
от них можно получить, когда тексты нескольких функций отличаются
один от другого только используемыми типами переменных, а в
остальном совпадают. Для такого единообразия при обращении с
последовательностями элементов было изобретено понятие итератора.
Итератор — это объект, по смыслу очень напоминающий указатель.
Как и указатель, он служит для выбора одного из элементов некоторой
последовательности. Операции, которые с ним можно выполнять, по виду
очень похожи на арифметику указателей, но реализация может сильно
отличаться. Итератор используется для последовательного перебора
элементов некоторой последовательности.
По своим возможностям итераторы делятся на несколько классов:
1) Итераторы ввода. Это простейший вид итератора. Такие
итераторы позволяют выполнять над ними всего три действия:
разыменование (при помощи * и ->, если элемент последовательности
имеет структурный тип), сдвиг на одну позицию вперед, причем между
двумя соседними сдвигами допускается не более одного вызова операции
разыменования, и сравнение на равенство. Такие итераторы описывают
последовательности самого общего вида. Например, при помощи таких
итераторов можно даже описать процесс ввода элементов с клавиатуры,
почему, собственно, такие итераторы и называются итераторами ввода.
Аналогично итераторам ввода, имеются итераторы вывода (все то же
самое, за исключением того, что вместо операции разыменования
допускается лишь запись указываемого элемента, т. е. выражение вида
*i=..., где i — итератор).
2) Однонаправленные итераторы сочетают в себе возможности и
итераторов ввода, и итераторов вывода; кроме того, между двумя
сдвигами можно сколько угодно раз читать (разыменовывать) и
записывать указываемый итератором элемент. Такие итераторы обычно
используются для перебора элементов односвязного списка.
3) Двунаправленные итераторы умеют все то же, что и
однонаправленные, но еще они умеют сдвигаться назад на одну позицию
(операция декремента −−). Подходят для перебора элементов двусвязного
списка.
4) Наконец, самые умные итераторы — итераторы произвольного
доступа, они реализуют все возможности арифметики указателей.
Подходят для перебора элементов структур данных на основе массивов, и
в большинстве случаев представляют собой с точки зрения внутреннего
устройства обычные указатели.
Пример использования итератора дает следующая функция для
печати списка целых чисел:
void print(list<int> l)
{
list<int>::iterator i;
for(i = l.begin(); i != l.end(); i++)
cout<<*i<<endl;
}
Здесь используется тип итератора iterator, вложенный в класс
list<int>. Зачем он реализован, как вложенный тип, будет ясно чуть позже.
begin — метод шаблонного класса список, возвращающий итератор,
указывающий на первый элемент списка. end — тоже метод класса
список, возвращающий итератор, указывающий на фиктивный элемент,
как бы следующий за последним элементом списка. Здесь нужно также
обратить внимание на то, что в условии цикла нельзя писать <= —
двунаправленные итераторы нельзя сравнивать на больше-меньше, только
на равенство или отсутствие равенства.
Оказывается, только что написанную функцию можно обобщить,
чтобы она позволяла выводить список из любого типа элементов:
template<class T>
void print(list<T> l)
{
typename list<T>::iterator i;
for(i = l.begin(); i != l.end(); i++)
cout<<*i<<endl;
}
Здесь ключевое слово typename используется для того, чтобы дать
компилятору понять, что list<T>::iterator — это тип (поскольку в момент
обработки этого шаблона мы ничего не знаем о том, какой тип будем
подставлять вместо T, это выражение, вообще говоря, может быть чем
угодно вплоть до константы в перечислимом типе).
Перебор элементов в последовательности при помощи итератора
позволяет даже произвести над этой функцией еще один шаг обобщения и
получить функцию, печатающую элементы любой последовательности,
будь то вектор, список или что угодно, лишь бы в классе этой
последовательности имелся вложенный класс iterator и интерфейс работы
с ним был стандартный:
template<class T>
void print(T l)
{
typename T::iterator i;
for(i = l.begin(); i != l.end(); i++)
cout<<*i<<endl;
}
Именно ради возможности получить класс iterator по классу
последовательности T при помощи выражения T::iterator этот класс и
сделан вложенным.

Шаблоны классов и Заголовочные файлы


Шаблон не является ни классом, ни функцией — это трафарет,
используемый для создания классов или функций. Таким образом,
шаблоны работают не так, как обычные функции или классы. В
большинстве случаев это не является проблемой, но на практике
случаются разные ситуации.
Работая с обычными классами, мы помещаем определение класса
в заголовочный файл, а определения методов этого класса в отдельный
файл .cpp с аналогичным именем. Таким образом, фактическое
определение класса компилируется как отдельный файл внутри проекта.
Однако с шаблонами всё происходит несколько иначе. Рассмотрим
следующее:
Array.h:
Array.cpp:

main.cpp:
Программа выше скомпилируется, но вызовет следующую ошибку
линкера:
unresolved external symbol "public: int __thiscall
Array::getLength(void)" (?GetLength@?$Array@H@@QAEHXZ)
Почему так? Сейчас разберемся.
Для использования шаблона, компилятор должен видеть как
определение шаблона (а не только объявление), так и тип шаблона,
используемый для создания экземпляра шаблона. Помним, что язык C++
компилирует файлы по отдельности. Когда заголовочный файл Array.h
подключается в main.cpp, то определение шаблона класса копируется в
этот файл. В main.cpp компилятор видит, что нам нужны два экземпляра
шаблона класса: Array<int> и Array<double>, он создаст их, а затем
скомпилирует весь этот код как часть файла main.cpp. Однако, когда дело
дойдет до компиляции Array.cpp (отдельным файлом), компилятор
забудет, что мы использовали Array<int> и Array<double> в main.cpp и не
создаст экземпляр шаблона функции getLength(), который нам нужен для
выполнения программы. Поэтому мы получим ошибку линкера, так как
компилятор не сможет найти
определение Array<int>::getLength() или Array<double>::getLength().
Эту проблему можно решить несколькими способами.
Самый простой вариант — поместить код из Array.cpp в Array.h
ниже класса. Таким образом, когда мы будем подключать Array.h, весь
код шаблона класса (полное объявление и определение как класса, так и
его методов) будет находиться в одном месте. Плюс этого способа —
простота. Минус — если шаблон класса используется во многих местах,
то мы получим много локальных копий шаблона класса, что увеличит
время компиляции и линкинга файлов (линкер должен будет удалить
дублирование определений класса и методов, дабы исполняемый файл не
был «слишком раздутым»). Рекомендуется использовать это решение до
тех пор, пока время компиляции или линкинга не является проблемой.
Если вы считаете, что размещение кода из Array.cpp в Array.h
сделает Array.h слишком большим/беспорядочным, то альтернативой
будет переименование Array.cpp в Array.inl (.inl от англ. «inline» =
«встроенный»), а затем подключение Array.inl из нижней части файла
Array.h. Это даст тот же результат, что и размещение всего кода в
заголовочном файле, но, таким образом, код получится немного чище.
Есть еще решения с подключением файлов .cpp, но эти варианты не
рекомендуется использовать из-за нестандартного применения директивы
#include.
Еще один альтернативный вариант — использовать подход 3-х
файлов:
   Определение шаблона класса хранится в заголовочном файле.
   Определения методов шаблона класса хранятся в отдельном
файле .cpp.
   Затем добавляем третий файл, который содержит все необходимые
нам экземпляры шаблона класса.
Например, templates.cpp:
Часть template class заставит компилятора явно создать указанные
экземпляры шаблона класса. В примере, приведенном выше, компилятор
создаст Array<int> и Array<double> внутри templates.cpp. Поскольку
templates.cpp находится внутри нашего проекта, то он скомпилируется и
удачно свяжется с другими файлами (пройдет линкинг).
Этот метод более эффективен, но требует создания/поддержки
третьего файла (templates.cpp) для каждой из ваших программ (проектов)
отдельно.

Задание на лабораторную работу

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


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

Задания к лабораторной работе.


Во всех этих задачах нужно пользоваться шаблонным классом список из
стандартной библиотеки.
1. Ввести с клавиатуры имя файла, список целых чисел и вывести список
квадратов чисел из этого списка в указанный файл.
2. Ввести список целых чисел с клавиатуры, и вывести число единиц в
нем.
3. Ввести список целых чисел с клавиатуры и вывести его следующим
образом: четные элементы выводятся, деленные на 2, а остальные — без
изменений. При этом сам список не должен меняться.
4. Ввести список целых чисел из файла и записать четные числа из этого
списка (с сохранением порядка) в один файл, а нечетные — в другой.
5. Ввести список целых чисел с клавиатуры и вывести его элементы,
начиная с первой встречающейся единицы (если в списке нет единиц, то
ничего не выводится).
6. Написать функцию, принимающую список вещественных чисел и
возвращающую среднее арифметическое его элементов.
7. Написать функцию, принимающую список целых чисел и
возвращающую среднее арифметическое номеров всех пятерок, имеющихся
в списке.
8. Написать шаблонную функцию, принимающую список и значение
элемента и возвращающую число вхождений этого элемента в список.
9. Написать шаблонную функцию, принимающую список и
возвращающую список квадратов элементов списка-параметра.
10. Написать шаблонную функцию, принимающую список и указатель
на функцию типа bool(элемент списка) и возвращающую два списка из
элементов того же типа: один состоит из тех элементов исходного списка, на
которых функция-параметр выдает true, другой — false (в остальном порядок
элементов исходного списка сохраняется).
11. Написать шаблонную функцию, принимающую список и указатель
на функцию типа bool(int) и возвращающую два списка из элементов того же
типа: один состоит из тех элементов исходного списка, на номерах которых
функция-параметр выдает true, другой — false (в остальном порядок
элементов исходного списка сохраняется).
12. Ввести список целых чисел с клавиатуры, отсортировать его и
вывести на экран.
13. Ввести список целых чисел с клавиатуры, вставить перед каждым
элементом, равным единице, новый элемент со значением ноль и вывести
результат.
14. Ввести список целых чисел с клавиатуры, вставить после каждой
единицы ноль и вывести результат.
15. Ввести список целых чисел с клавиатуры, вставить между каждыми
соседними единицами ноль и вывести результат.
16. Ввести список целых чисел, удалить все элементы данных со
значениями 1, 2, 3 и вывести результат.
17. Ввести список целых чисел с клавиатуры, вывести его, удалить
элементы с номерами, кратными трем, и вывести результат.
18. Ввести два списка целых чисел с клавиатуры, вклеить второй из них
перед первой единицей в первом списке и вывести результат (без циклов).
19. Написать функцию, принимающую список целых чисел и указатель
на функцию типа int(int), и заменяющую каждый элемент списка на результат
применения к нему функции-параметра.
20. Написать шаблонную функцию, принимающую два упорядоченных
по возрастанию списка и возвращающую их упорядоченное объединение,
тоже как список (одинаковые элементы сохраняются, т. е. если число 5
присутствовало в одном списке 3 раза, а в другом 7 раз, то в результирующем
списке оно должно присутствовать 10 раз).

Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
 Титульный лист.
 Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
 Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
 Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
 Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
 Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
 Выводы по лабораторной работе.
 Ответы на контрольные вопросы.

Контрольные вопросы:
16.Что из себя представляют шаблонные классы?
17.Синтаксис шаблонного класса
18.Опишите готовые шаблонные классы для стека (файл stack),
очереди (файл queue), вектор (файл vector), связный (более точно,
двусвязный) список
19.Что такое итератор?
20.Опишите итераторы ввода.
21.Опишите Однонаправленные итераторы
22.Опишите Двунаправленные итераторы
23.Опишите итераторы произвольного доступа
24.Приведите пример использования итератора
Лабораторная работа № 11. Реализация классов. Конструкторы и
деструкторы классов. Перегрузка и переопределение методов
Цель: ознакомится с реализацией классов, изучить конструкторы и
деструкторы классов.

Теоретическая часть

Конструктор и деструктор класса в C++


Возможно вы заметили, что определяя класс, мы не можем
инициализировать его поля (члены) в самом определении. Можно присвоить
им значение, написав соответствующий метод класса и вызвав его, после
создания объекта вне класса. Такой способ не совсем удобен, так как
объявляя, допустим,  33 объекта класса нам придется 33 раза вызывать метод,
который присваивает значения полям класса. Поэтому, как правило, для
инициализации полей класса, а так же для выделения динамической памяти,
используется конструктор.
Конструктор и деструктор
При создании объектов одной из наиболее широко используемых
операций, которую вы будете выполнять в ваших программах, является
инициализация элементов данных объекта. Единственным способом, с
помощью которого вы можете обратиться к частным элементам данных,
является использование функций класса. Чтобы упростить процесс
инициализации элементов данных класса, C++ использует специальную
функцию, называемую конструктором, которая запускается для каждого
создаваемого вами объекта. Подобным образом C++ обеспечивает функцию,
называемую деструктором, которая запускается при уничтожении объекта.
Таким образом:
• Конструктор представляет собой метод класса, который облегчает
вашим программам инициализацию элементов данных класса при
объявлении объекта.
• Конструктор имеет такое же имя, как и класс.
• Конструктор не имеет возвращаемого значения.
• Каждый раз, когда ваша программа создает переменную класса, C++
вызывает конструктор класса.
• Многие объекты могут распределять память для хранения
информации; когда вы уничтожаете такой объект, C++ будет вызывать
специальный деструктор, который может освобождать эту память, очищая ее
после объекта.
• Деструктор имеет такое же имя, как и класс, за исключением того, что
вы должны предварять его имя символом тильды (~).
• Деструктор не имеет возвращаемого значения.
Термины конструктор и деструктор не должны вас пугать. Вместо
этого представьте конструктор как функцию, которая помогает вам строить
(конструировать) объект. Подобно этому, деструктор представляет собой
функцию, которая помогает вам уничтожать объект. Деструктор обычно
используется, если при уничтожении объекта нужно освободить память,
которую занимал объект.

Конструктор (от construct – создавать) – это особый метод класса,


который выполняется автоматически в момент создания объекта класса. То
есть, если мы пропишем в нем, какими значениями надо инициализировать
поля во время объявления объекта класса, он сработает без “особого
приглашения”. Его не надо специально вызывать, как обычный метод класса.
Пример:
1 #include <iostream>
2 using namespace std;
3  
4 class SomeData
5 {
6 private:
7 int someNum1;
8 double someNum2;
9 char someSymb[128];
10 public:
SomeData() //это конструктор:
11
{
12
someNum1 = 111; // присваиваем начальные значения полям
13
someNum2 = 222.22;
14
strcpy_s(someSymb, "СТРОКА!");
15
cout << "\nКонструктор сработал!\n";
16
}
17
 
18
void showSomeData()
19
{
20
cout << "someNum1 = " << someNum1 << endl;
21
cout << "someNum2 = " << someNum2 << endl;
22
cout << "someSymb = " << someSymb << endl;
23
}
24
}obj1; //уже на этом этапе сработает конструктор (значения запишутся в
25
поля)
26
 
27
int main()
28
{
29
setlocale(LC_ALL, "rus");
30
obj1.showSomeData();
31
 
32
SomeData obj2; //тут сработает конструктор для второго объекта класса
33
obj2.showSomeData();
34
}
В строках 11 – 17 определяем конструктор: имя должно быть
идентично имени класса;  конструктор НЕ имеет типа возвращаемого
значения (void в том числе). Один объект объявляется сразу во время
определения класса – строка 25. При запуске программы, конструктор этого
объекта сработает даже до входа в главную функцию. Это видно на
следующем снимке:
программа еще не дошла до выполнения строки
29 setlocale(LC_ALL, "rus");  , а конструктор уже “отчитался”, что сработал
(кириллица отобразилась некорректно). В строке 30 – смотрим, что содержат
поля класса. Второй раз конструктор сработает в строке 32, во время
создания объекта obj2.
Создание простого конструктора
Конструктор представляет собой метод класса, который имеет такое
же имя, как и класс. Например, если вы используете класс с именем
employee, конструктор также будет иметь имя employee. Подобно этому, для
класса с именем dogs конструктор будет иметь имя dogs. Если ваша
программа определяет конструктор, C++ будет автоматически вызывать его
каждый раз, когда вы создаете объект. Следующая программа
CONSTRUC.CPP создает класс с именем employee. Программа также
определяет конструктор с именем employee, который присваивает начальные
значения объекту. Однако конструктор не возвращает никакого значения,
несмотря на то, что он не объявляется как void. Вместо этого вы просто не
указываете тип возвращаемого значения:

class employee {
  public:
    employee(char *, long, float); // объявление конструктора
    void show_employee(void);
    int change_salary(float);
    long get_id(void);
  private:
    char name[64];
    long employee_id;
    float salary;
};
В вашей программе вы просто определяете конструктор так же, как
любой другой метод класса:

employee::employee(char *name, long employee_id, float salary)


{
strcpy(employee::name, name);
employee::employee_id = employee_id;
if (salary < 50000.0)
   employee::salary = salary;
else
employee::salary = 0.0; // Недопустимый оклад
}
Как видите, конструктор не возвращает значение вызвавшей функции.
Для него также не используется тип void. В данном случае конструктор
использует оператор глобального разрешения и имя класса перед именем
каждого элемента. Ниже приведена реализация программы CONSTRUC.CPP:

#include <iostream.h>
#include <string.h>
class employee {
public:
  employee (char *, long, float);
  void show_employee(void);
  int change_salary ( float );
  long get_id(void);
private :
  char name [64] ;
  long employee_id;
  float salary;
};
employee:: employee (char *name, long employee_id, float salary)
{
strcpy ( employee :: name , name ) ;
employee :: employee_id = employee_id;
if (salary < 50000.0)
  employee :: salary = salary;
else
employee :: salary = 0.0;  // Недопустимый оклад
}
void employee:: show_employee (void)
{
cout << "Служащий: " << name << endl;
cout << "Номер служащего: " << employee_id << endl;
cout << "Оклад: " << salary << endl;
}
void main (void)
{
employee worker ( "Happy Jamsa", 101, 10101.0);
worker . show_employee ( ) ;
}
Обратите внимание, что за объявлением объекта worker следуют
круглые скобки и начальные значения, как и при вызове функции. Когда вы
используете конструктор, передавайте ему параметры при объявлении
объекта:
employee worker ( "Happy Jamsa", 101, 10101.0);
Если вашей программе потребуется создать несколько объектов
employee, вы можете инициализировать элементы каждого из них с помощью
конструктора, как показано ниже:
employee worker ( "Happy Jamsa", 101, 10101.0);
employee secretary ("John Doe", 57, 20000.0);
employee manager ( "Jane Doe", 1022, 30000.0);

Конструкторы и параметры по умолчанию


C++ позволяет указывать значения по умолчанию для параметров
функции. Если пользователь не указывает каких-либо параметров, функция
будет использовать значения по умолчанию. Конструктор не является
исключением; ваша программа может указать для него значения по
умолчанию так же, как и для любой другой функции. Например, следующий
конструктор employee использует по умолчанию значение оклада равным
10000.0, если программа не указывает оклад при создании объекта. Однако
программа должна указать имя служащего и его номер:

employee:: employee (char *name, long employee_id, float salary =  10000.00)


{
strcpy ( employee :: name , name ) ;
employee :: employee_id = employee_id;
if (salary < 50000.0)
employee :: salary = salary;
else
employee :: salary  =  0.0;  //  Недопустимый оклад
}
Перегрузка конструкторов
Как вы уже знаете, C++ позволяет вашим программам перегружать
определения функций, указывая альтернативные функции для других типов
параметров. C++ позволяет вам также перегружать конструкторы.
Следующая программа CONSOVER.CPP перегружает конструктор employee.
Первый конструктор требует, чтобы программа указывала имя служащего,
номер служащего и оклад:

employee:: employee (char *name, long employee_id, float salary)


{
strcpy ( employee :: name , name ) ;
employee :: employee_id = employee_id;
if (salary < 50000.0)
  employee :: salary = salary;
else
employee :: salary = 0.0;  // Недопустимый оклад
}
Второй конструктор запрашивает пользователя ввести требуемый
оклад, если программа не указывает его:

employee::employee(char *name, long employee_id)


{
strcpy(employee::name, name);
employee::employee_id = employee_id;
do {
       cout << "Введите оклад для " << name <<" меньше $50000: ";
       cin >> employee: :salary;
     }
while (salary >= 50000.0);
}
Внутри определения класса программа должна указать прототипы для
обоих конструкторов, как показано ниже:

class employee {
public:
   employee (char *, long, float) ;   // Прототипы перегруженных
конструкторов
   employee (char *, long);            // Прототипы перегруженных
конструкторов
   void show_employee(void) ;
   int change_salary ( float );
   long get_id(void) ;
private :
char name [64] ;
long employee_id;
float salary;
}
Ниже приведена реализация программы CONSOVER.CPP:

# include <iostream.h>
# include <string.h>
class employee {
public:
   employee (char *, long, float);
   employee (char *, long);
   void show_employee(void) ;
   int change_salary( float) ;
   long get_id(void) ;
private :
char name [64] ;
long employee_id;
float salary;};

employee:: employee (char *name, long employee_id,float salary)


{
strcpy ( employee :: name , name ) ;
employee :: employee_id = employee_id;
if (salary < 50000.0) employee::salary = salary;
else      // Недопустимый оклад
employee::salary = 0.0;}

 employee::employee(char *name, long employee_id)


{
strcpy(employee::name, name);
employee::employee_id = employee_id;
do {
       cout << "Введите оклад для " << name <<" меньше $50000: ";
       cin >> employee: :salary;
     }
while (salary >= 50000.0);
}

void employee::show_employee(void)
{
cout << "Служащий: " << name << endl;
cout << "Номер служащего: " << employee_id << endl;
cout << "Оклад: " << salary << endl;
}

void main(void)
{
employee worker("Happy Jamsa", 101, 10101.0);
employee manager("Jane Doe", 102);
worker.show_employee();
manager.show_employee();
}
Если вы откомпилируете и запустите эту программу, на вашем экране
появится запрос ввести оклад для Jane Doe. Когда вы введете оклад,
программа отобразит информацию об обоих служащих.
Конструктор копирования
Конструктор копирования создает объект класса, копируя при этом
данные из уже существующего объекта данного класса.

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


принимает в качестве параметра ссылку или константную ссылку на объект
данного класса. Он автоматически вызывается компилятором, когда вновь
создаваемый объект инициализируется значениями существующего объекта.
Пример:
class Coord
{int x,y;
public:
Coord(const Coord & src)   //объявление конструктора копирования
};
Coord:: Coord(const Coord & src)    // определение конструктора
копирования
{
x = src.x;
y = src.y;
}
int main()
{ Coord ob1(2,9); //создание объекта 1
Coord ob2 = ob1; //копирование объекта 1 в объект 2
Coord ob3(ob1); // копирование объекта 1 в объект 3
….
return 0;
}
Если конструктор копирования специально не создавался, компилятор
создает его по умолчанию. Этот конструктор вызывается в тех случаях, когда
новый объект создается путем копирования существующего:
 при описании нового объекта с инициализацией другим
объектом;
 при передаче объекта в функцию по значению;
 при возврате объекта из функции, а также при обработке
исключений.
Такой конструктор выполняет поэлементное копирование полей.
Если класс содержит данные в виде указателей или ссылок
использование в этом случае конструктора копирования, созданного по
умолчанию будет бессмысленным или опасным, поскольку и копия, и
оригинал будут указывать на одну и ту же область памяти. В этом случае
необходимо специально создавать конструктор копирования.
Запишем конструктор копирования для класса monstr. Поскольку в нем
есть поле name, содержащее указатель на строку символов, конструктор
копирования должен выделять память под новую строку и копировать в нее
исходную:
class monstr
{
      int health, ammo;
      color skin;
      char *name;
public:
      monstr(int he = 100, int am   = 10);
      monstr (color sk);
      monstr(char * nam) ;
      int get_health(){ return health:}
      int get_ammo(){ return ammo;}
}
……………………
monstr: :monstr(const monstr &M)
{
 if (M.name)
   {
      name = new char [strlen(M.name) + 1];
      strcpy(name, M.name);
    }
 else name = 0;
 health = M. health;
 ammo = M.ammo;
 skin = M.skin;
}
…………
monstr Vasia (blue);
monstr Super = Vasia;       // Работает конструктор копирования
monstr *m = new monstr ("Ork");
monstr Green = *m;        // Работает конструктор копирования

Деструктор
Деструктор (от destruct – разрушать) – так же особый метод класса,
который срабатывает во время уничтожения объектов класса. Чаще всего его
роль заключается в том, чтобы освободить динамическую память, которую
выделял конструктор для объекта. Имя его, как и у конструктора, должно
соответствовать имени класса. Только перед именем надо добавить символ ~
Деструктор – это особый вид метода, применяющийся для
освобождения памяти, занимаемой объектом. Деструктор вызывается
автоматически, когда объект выходит из области видимости:
 для локальных объектов – при выходе из блока, в котором они
объявлены;
 для глобальных – как часть процедуры выхода из main при
завершении программы;
 для объектов, заданных динамически через указатели, деструктор
вызывается неявно при использовании операции delete.
Автоматический вызов деструктора объекта при выходе из области
действия указателя на него не производится.
Имя деструктора начинается с тильды (~), непосредственно за которой
следует имя класса:
~class_name(void)
{

Операторы деструктора
}
В отличие от конструктора вы не можете передавать параметры
деструктору.
Следующий код определяет деструктор для класса employee:
void employee:: ~employee (void)
{
cout << "Уничтожение объекта для " << name << endl;
}
В данном случае деструктор просто выводит на ваш экран сообщение о
том, что C++ уничтожает объект.
Деструктор:
 не имеет аргументов и возвращаемого значения;
 не может быть объявлен как const или static;
 не наследуется;
 может быть виртуальным;
 указатель на деструктор определить нельзя.
Если деструктор явным образом не определен, компилятор
автоматически создает пустой деструктор.
Описывать в классе деструктор явным образом требуется в случае,
когда объект содержит указатели на память, выделяемую динамически –
иначе при уничтожении объекта память, на которую ссылались его поля-
указатели, не будет помечена как свободная.
Деструктор для класса monstr должен выглядеть так:
monstr::~monstr() {delete [] name;}
Деструктор можно вызвать явным образом путем указания полностью
уточненного имени, например:
monstr *m=new monstr;  //создается динамический объект
m -> ~monstr();  // явно вызывается деструктор
Это может понадобиться для объектов, которым с помощью операции
new выделялся конкретный адрес памяти. Без необходимости явно вызывать
деструктор объекта не рекомендуется.
Конструкторы и деструкторы в С++ вызываются автоматически, что
гарантирует правильное создание и удаление объектов класса.

Добавим деструктор в предыдущий код. И создадим в классе два


конструктора: один будет принимать параметры, второй – нет.

1 #include <iostream>
2 using namespace std;
3  
4 class SomeData
5 {
private:
6
int someNum1;
7
double someNum2;
8
char someSymb[128];
9
public:
10
SomeData()
11
{
12
someNum1 = 0;
13
someNum2 = 0;
14
strcpy_s(someSymb, "СТРОКА ПО УМОЛЧАНИЮ!");
15
cout << "\nКонструктор сработал!\n";
16
}
17
 
18
SomeData(int n1, double n2, char s[])
19
{
20
someNum1 = n1;
21
someNum2 = n2;
22
strcpy_s(someSymb, s);
23
cout << "\nКонструктор с параметрами сработал!\n";
24
}
25
 
26
void showSomeData()
27
{
28
cout << "someNum1 = " << someNum1 << endl;
29
cout << "someNum2 = " << someNum2 << endl;
30
cout << "someSymb = " << someSymb << endl;
31
}
32
 
33
~SomeData()
34
{
35
cout << "\nДеcтруктор сработал!\n";
36
}
37
};
38
 
39
int main()
40
{
41
setlocale(LC_ALL, "rus");
42
SomeData obj1(1, 2.2, "СТРОКА ПАРАМЕТР"); // сработает конструктор с
43
параметрами
44
obj1.showSomeData();
45
 
46
SomeData obj2; // сработает конструктор по умолчанию
47
obj2.showSomeData();
48
}
Деструктор определен в строках 34 – 37. Для простоты примера он
просто отобразит строку в том месте программы, где сработает. Строка 43 –
объявляем объект класса и передаем данные для записи в поля. Тут сработает
конструктор с параметрами. А в строке 46 – сработает конструктор по
умолчанию.

Видим, что деструктор сработал автоматически и дважды (так как в


программе было два объекта класса). Он срабатывает тогда, когда работа
программы завершается и уничтожаются все данные.
Важное:
o Конструктор и деструктор должны быть  public;
o Конструктор и деструктор не имеют типа возвращаемого
значения;
o Имена класса, конструктора и деструктора должны совпадать;
o Конструктор может принимать параметры. Деструктор не
принимает параметры;
o При определении деструктора перед именем надо добавить
символ   «~»;
 Конструкторов может быть несколько, но их сигнатура должна
отличаться (количеством принимаемых параметров, например);
 Деструктор в классе должен быть определен только один.
Примеры решения задач
Решение задачи разбивается на три файла. В первом файле («BitSet.h»)
содержится описание класс. Во втором файле («BitSet.cpp») содержится
реализация методов. В последнем («main.cpp») содержится пример
использования разработанного класса. В примере использования класса
демонстрируется обработка исключений.
Пример заголовочного файла с описанием класса.
BitSet.h
#ifndef __BIT_SET__
#define __BIT_SET__

class BitSet
{
private:
int *bits;
public:
//проверка – установлен ли бит в множестве
bool isSet(int i);
//конструктор класса битовых множеств
BitSet();
//деструктор класса битовых множеств
~BitSet();
};

//Структура для обработки исключений и коды ошибок


#define __OutOfRange__ -1

struct ExceptionBitSet{
int index;
int ErrorCode;
ExceptionBitSet(int _index, int _ErrorCode):
index(_index), ErrorCode(_ErrorCode){}
};

#endif

Пример файла с реализацией методов.


BitSet.cpp
#include "BitSet.h"

//проверка – установлен ли бит в множестве


bool BitSet::isSet(int i)
{
//проверка исключительных ситуаций
if((i < 0) || (i >= 100) )
throw ExceptionBitSet(i, __OutOfRange__);

int ii, jj;


ii = i / 32;
jj = i % 32;
int val = bits[ii] & (~(1<<jj));
return val != 0;
}

//конструктор класса битовых множеств


BitSet::BitSet()
{
bits = new int[4];
for(int i = 0; i < 4; i++)
{
bits[i] = 0;
}
}

//деструктор класса битовых множеств


BitSet::~BitSet()
{
delete[]bits;
}

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


main.cpp
#include "BitSet.h"
#include <stdio.h>

int main()
{
//создано два объекта класса
BitSet b1, b2;
//проверка значения бита множества
//и всех исключительных ситуаций
try
{
b1.isSet(10);
printf("OK\n");
} catch (ExceptionBitSet e)
{
printf("Exception: index %d code %d\n", e.index, e.ErrorCode);
}
try
{
b1.isSet(-1);
printf("OK\n");
} catch (ExceptionBitSet e)
{
printf("Exception: index %d code %d\n", e.index, e.ErrorCode);
}
try
{
b1.isSet(100);
printf("OK\n");
} catch (ExceptionBitSet e)
{
printf("Exception: index %d code %d\n", e.index, e.ErrorCode);
}

return 0;
}

Задания к лабораторной работе.


На основе примера решения задач выполнить задание по своему
варианту.
Общая постановка задач
В нижеприведенных задачах необходимо разработать объявление
класса (или классов) в виде заголовочного файла и описание класса в виде
исполняемого файла. В объявлении и описании класса должны быть
реализованы указанные методы. При реализации методов необходимо
реализовать указанные исключения. Также необходимо разработать
необходимые конструкторы и при необходимости деструктор.
Тестовая функция main должна находиться в отдельном фале и
содержать объявления не менее двух объектов тестируемого класса. При
использовании методов объекта необходимо продемонстрировать обработку
исключительной ситуации.
Варианты
1. Разработайте класс вектор из 10 элементов и реализуйте метод
инициализации элемента по индексу. В классе реализовать конструктор по
умолчанию и деструктор. Память в объекте заводится динамически. При
реализации методов предполагается, что методы нельзя вызвать, если индекс
меньше нуля.
2. Разработайте класс вектор из 10 элементов и реализуйте метод
сложения элементов векторов. В классе реализовать конструктор
копирования и деструктор. Память в объекте заводится не динамически. При
реализации методов предполагается, что методы нельзя вызвать, если
размеры суммируемых векторов не совпадают.
3. Разработайте класс вектор из 10 элементов и реализуйте метод
вычитания элементов вектора из заданного числа. В классе реализовать
инициализирующий конструктор и деструктор. Память в объекте заводится
не динамически. При реализации методов предполагается, что методы нельзя
вызвать, если значения вектора не инициализированы.
4. Разработайте класс вектор из 10 элементов и реализуйте метод
скалярного произведения векторов. В классе реализовать
инициализирующий конструктор и деструктор. Память в объекте заводится
динамически. При реализации методов предполагается, что методы нельзя
вызвать, если значение текущего вектора не инициализировались.
5. Разработайте класс матрица размерности 3x3 и реализуйте метод
сложения элементов матрицы. Матрицу хранить как непрерывный массив. В
классе реализовать инициализирующий конструктор и деструктор. Память в
объекте заводится не динамически. При реализации методов предполагается,
что методы нельзя вызвать, если числа матрицы отрицательные.
6. Разработайте класс матрица размерности 3x3 и реализуйте метод
вычитания элементов матрицы из заданного числа. Матрицу хранить как
непрерывный массив. В классе реализовать конструктор по умолчанию и
деструктор. Память в объекте заводится динамически. При реализации
методов предполагается, что методы нельзя вызвать, если матрица не
симметричная.
7. Разработайте класс матрица размерности 3x3 и реализуйте метод
умножения матриц. Матрицу хранить как непрерывный массив. В классе
реализовать конструктор копирования и деструктор. Память в объекте
заводится динамически. При реализации методов предполагается, что методы
нельзя вызвать, если размерности матриц не совпадают.
8. Разработайте класс прямоугольник. В разработанном классе
реализовать метод вычисления площади прямоугольника. В классе
реализовать конструктор копирования и деструктор. При реализации методов
предполагается, что методы нельзя вызвать, если в результате вычислений
площадь получилась отрицательной.
9. Разработайте класс прямоугольник. В разработанном классе
реализовать метод позволяющий вычислить площадь фигуры полученной в
результате объединения прямоугольников. В классе реализовать
инициализирующий конструктор и деструктор. При реализации методов
предполагается, что методы нельзя вызвать, если прямоугольники не имеют
общих точек.
10. Разработайте класс прямоугольник. В разработанном классе
реализовать метод позволяющую вычислить площадь фигуры полученной в
результате пересечения прямоугольников. В классе реализовать конструктор
по умолчанию и деструктор. При реализации методов предполагается, что
методы нельзя вызвать, если вычисленная площадь равна нулю.

Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
 Титульный лист.
 Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
 Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
 Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
 Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
 Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
 Выводы по лабораторной работе.
 Ответы на контрольные вопросы.

Контрольные вопросы:
1. Что такое конструктор класса?
2. Для чего предназначен конструктор?
3. Какими свойствами обладает конструктор?
4. Сколько конструкторов может быть у класса?
5. По какому признаку в тексте программы можно найти
конструктор?
6. Когда вызывается конструктор? Может ли он быть вызван явно?
7. Какие формы инициализации полей класса возможны при
использовании конструкторов?
8. Что такое «стандартный конструктор»? Когда он используется?
9. Что представляет собой конструктор копирования? Когда он
используется?
10. Что такое «стандартный конструктор копирования»? Когда он
используется?
11. В каком случае у класса обязательно должен быть конструктор
копирования?
12. Сколько и какие конструкторы содержит класс, в котором нет ни
одного явно определенного конструктора?
13. Что такое деструктор класса?
14. Для чего предназначен деструктор?
15. Сколько деструкторов может быть у класса?
16. Когда вызывается деструктор? Может ли он быть вызван явно?
17. Вызывается ли деструктор, если из области видимости выходит
динамический объект, адресуемый указателем?
Лабораторная работа № 12. Реализация классов. Дружественные и
виртуальные функции.

Цель: ознакомится с дружественными и виртуальными функциями,


реализовать классы дружественных и виртуальных функций.

Теоретическая часть

Виртуальные функции
Виртуальные функции являются важной частью реализации механизма
полиморфизма (то есть выполнения разных действий с объектами разного
типа). Использование виртуальных функций позволяет выполнить нужные
действия в том случае, если доступ к объекту осуществляется не по
значению, а по указателю.
Пусть имеется класс «Фигура», и имеются его классы-наследники
«Круг», «Квадрат» и «Треугольник». Пусть в каждом из этих классов есть
функция draw( ), которая прорисовывает фигуры на экране.
Теперь пусть имеется указатель X на объект класса «Фигура». По
правилам языка этот указатель может хранить адрес как самого класса
«Фигура», так и адрес любого из его потомков, т.е. и «Круга» и «Квадрата» и
«Треугольника». Теперь необходимо реализовать функцию draw( ) так, чтобы
при обращении к ней через указатель X->draw( ) вызывалась бы именно
нужная функция draw( ), то есть именно того класса, адрес которого и
хранится в указателе X. Понятно, что у круга, квадрата и треугольника это
будут разные функции. Чтобы обеспечить такую возможность для функции
draw( ) её необходимо объявить виртуальной в базовом классе.
Ниже приводятся примеры, демонстрирующие возможности
виртуальной функции.
Первый пример показывает, что бывает, когда базовый и производный
классы содержат функции с одним и тем же именем, и к ним обращаются с
помощью указателей, но без использования виртуальных функций.
class Base //Базовый класс
{
public:
void show() //Обычная функция
{ cout << "Base\n"; }
};

class Derv1 : public Base //Производный класс 1


{
public:
void show()
{ cout << "Derv1\n"; }
};

class Derv2 : public Base //Производный класс 2


{
public:
void show()
{ cout << "Derv2\n"; }
};

int main()
{
Derv1 dv1; //Объект производного класса 1
Derv2 dv2; //Объект производного класса 2
Base* ptr; //Указатель на базовый класс

ptr = &dv1; //Адрес dv1 занести в указатель


ptr->show(); //Выполнить show()
ptr = &dv2; //Адрес dv2 занести в указатель
ptr->show(); //Выполнить show()
return 0;
}

Поскольку указатели на объекты дочерних классов совместимы по


типу с указателями на объекты базового класса присвоение ptr = &dv1; и ptr
= &dv2; вполне корректно. В результате выполнения приведенной
программы будет дважды вызвана функция show( ) именно базового класса,
так как компилятор в данном случае не смотрит на содержимое указателя ptr
, а выбирает тот метод, который удовлетворяет типу указателя (см. рисунок).
Рисунок 1. Доступ через указатель без использования виртуальных
функций

Теперь изменим обсуждаемую программу таким образом, чтобы


функция show( ) базового класса была бы объявлена как виртуальная
функция (то есть добавим перед её объявлением в базовом классе ключевое
слово virtual).

class Base //Базовый класс


{
public:
virtual void show() //Виртуальная функция
{ cout << "Base\n"; }
};

class Derv1 : public Base //Производный класс 1


{
public:
void show()
{ cout << "Derv1\n"; }
};
class Derv2 : public Base //Производный класс 2
{
public:
void show()
{ cout << "Derv2\n"; }
};

int main()
{
Derv1 dv1; //Объект производного класса 1
Derv2 dv2; //Объект производного класса 2
Base* ptr; //Указатель на базовый класс

ptr = &dv1; //Адрес dv1 занести в указатель


ptr->show(); //Выполнить show()
ptr = &dv2; //Адрес dv2 занести в указатель
ptr->show(); //Выполнить show()
return 0;
}

На выходе, в отличие от первого примера, будем иметь


Derv1
Derv2
то есть при вызове функции show( ) будут выполнены методы именно
соответствующих производных классов, а не базового. То есть один и тот же
вызов ptr->show(); запускает на выполнение разные функции в зависимости
от содержимого указателя ptr. Компилятор выбирает функцию,
удовлетворяющую тому, что занесено в указатель, а не типу указателя, как в
первом примере (см. рисунок).
Рисунок 2. Доступ через указатель к виртуальным функциям.

Абстрактные классы и чистые виртуальные функции


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

class Base //базовый класс


{
public:
virtual void show() = 0; //чистая виртуальная функция
};

class Derv1 : public Base //порожденный класс 1


{
public:
void show()
{ cout << ?Derv1\n?; }
};

class Derv2 : public Base //порожденный класс 2


{
public:
void show()
{ cout << ?Derv2\n?; }
};

int main()
{
// Base bad; //невозможно создать объект
//из абстрактного класса
Base* arr[2]; //массив указателей на
//базовый класс
Derv1 dv1; //Объект производного класса 1
Derv2 dv2; //Объект производного класса 2

arr[0] = &dv1; //Занести адрес dv1 в массив


arr[1] = &dv2; //Занести адрес dv2 в массив

arr[0]->show(); //Выполнить функцию show()


arr[1]->show(); //над обоими объектами
return 0;
}
Знак равенства при объявлении чистой виртуальной функции virtual
void show() = 0; это не операция присваивания, это просто способ сообщить
компилятору, что функция будет чистой виртуальной. Если теперь
попытаться создать объект этого класса, компилятор выдаст ошибку.
На практике механизм виртуальных функций часто применяется для
корректного вызова деструкторов. Технология программирования прямо
предписывает, что деструкторы базовых классов должны быть
виртуальными. Если деструктор базового класса не является виртуальным,
то, будучи обычным методом, он будет вызываться независимо от того,
данные какого типа в нем хранятся. Это приведет к тому, что может быть
удалена только та часть объекта, которая относится к базовому классу.
Конечно, если ни один из деструкторов (ни у базового, ни у производного
класса) ничего особенного не делает, тогда их виртуальность перестает быть
такой уж необходимой.

Дружественные функции
Принцип инкапсуляции и ограничения доступа к данным запрещает
функциям, не являющимися методами соответствующего класса, доступ к
скрытым (private) или защищенным (protected) данным объектов. Однако,
бывают ситуации, когда такая жесткая политика приводит к неудобствам.
Предположим, что необходимо реализовать функцию, которая могла
бы работать с объектами двух разных классов. В этой ситуации
целесообразно прибегнуть к механизму дружественных функций.
Пример
class alpha
{
private:
int data;
public:
alpha() : data(3) { } //конструктор без аргументов
friend int frifunc(alpha, beta); //дружественная функция
};

class beta
{
private:
int data;
public:
beta() : data(7) { } //конструктор без аргументов
friend int frifunc(alpha, beta); //дружественная функция
};

int frifunc(alpha a, beta b) //определение функции


{
return( a.data + b.data );
}

int main()
{
alpha aa;
beta bb;
cout << frifunc(aa, bb) << endl; //вызов функции
return 0;
}

В приведенном примере функция frifunc(aa, bb) имеет доступ к


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

class alpha
{
private:
int data1;
public:
alpha() : data1(99) { } //конструктор
friend class beta; //beta – дружественный класс
};

class beta
{ //все методы имеют доступ
public: //к скрытым данным alpha
void func1(alpha a) { cout << "\ndata1=" << a.data1;}
void func2(alpha a) { cout << "\ndata1=" << a.data1;}
};

int main()
{
alpha a;
beta b;

b.func1(a);
b.func2(a);
cout << endl;
return 0;
}

Здесь класс beta провозглашен дружественным для класса alpha.


Теперь все методы класса beta имеют доступ к скрытым данным класса
alpha.

Задания к лабораторной работе.


Задание 1. Ответить на контрольные вопросы.
Задание 2. Создать абстрактный класс Figure с виртуальными
методами вычисления площади и периметра. Создать производные классы:
Rectangle (прямоугольник), Circle (круг), Triangle (треугольник). Описать в
производных классах функции вычисления периметра и площади,
продемонстрировать работу механизма виртуальных функций.
Задание 3. В любой визуальной среде создать класс Figure с
виртуальным методом draw( ), осуществляющим прорисовку объекта на
визуальном компоненте. Создать производные классы: Rectangle
(прямоугольник), Circle (круг), Triangle (треугольник). Описать в
производных классах функции draw( ) для каждой из фигур,
продемонстрировать работу механизма виртуальных функций.

Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
• Титульный лист.
• Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
• Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
• Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
• Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
• Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
• Выводы по лабораторной работе.
• Ответы на контрольные вопросы.

Контрольные вопросы:
1. Какие возможности перед программистом открывают виртуальные
функции?
2. Истинно ли утверждение о том, что указатель на базовый класс
может ссылаться на объекты порожденного класса?
3. Пусть указатель p ссылается на объекты базового класса и содержит
адрес объекта порожденного класса. Пусть в обоих этих классах имеется
невиртуальный метод ding(). Тогда выражение p->ding() вызовет метод ding()
из ……… класса.
4. Напишите описание для виртуальной функции dang(),
возвращающей результат void и имеющей аргумент типа int.
5. Пусть указатель p ссылается на объекты базового класса и содержит
адрес объекта порожденного класса. Пусть в обоих этих классах имеется
виртуальный метод ding(). Тогда выражение p->ding() вызовет метод ding() из
……… класса.
6. Напишите описание для чистой виртуальной функции aragorn(), не
возвращающей значений и не имеющей аргументов.
7. Чистая виртуальная функция, это виртуальная функция, которая:
а) делает свой класс абстрактным;
б) не возвращает результата;
в) используется в базовом классе;
г) не имеет аргументов.
8. Напишите определение массива parr, содержащего 10 указателей на
объекты класса dong.
9. Абстрактный класс используется тогда, когда
а) не планируется создавать порожденные классы;
б) есть несколько связей между двумя порожденными классами
в) необходимо запретить создавать объекты класса
10. Истинно ли утверждение о том, что дружественная функция имеет
доступ к скрытым данным класса, даже не являясь его методом?
11. Напишите описание дружественной функции harry(),
возвращающей результат типа void и имеющей один аргумент класса george.
12. Ключевое слово friend появляется в:
а) классе, разрешающем доступ к другому классу;
б) классе, требующем доступа к другому классу;
в) разделе скрытых компонентов класса;
г) разделе общедоступных компонентов класса.

Вам также может понравиться