Открыть Электронные книги
Категории
Открыть Аудиокниги
Категории
Открыть Журналы
Категории
Открыть Документы
Категории
Ознакомление с рекурсиями и их
программная реализация
Теоретическая часть
Краткие итоги
1. Свойством рекурсивности характеризуются объекты
окружающего мира, обладающие самоподобием.
2. Рекурсия в широком смысле характеризуется определением
объекта посредством ссылки на себя.
3. Рекурсивные функции содержат в своем теле обращение к самим
себе с измененным набором параметров. При этом обращение к себе может
быть организовано через цепочку взаимных обращений функций.
4. Решение задач рекурсивными способами проводится
посредством разработки рекурсивной триады.
5. Целесообразность применения рекурсии в программировании
обусловлена спецификой задач, в постановке которых явно или опосредовано
указывается на возможность сведения задачи к подзадачам, аналогичным
самой задаче.
6. Область памяти, предназначенная для хранения всех
промежуточных значений локальных переменных при каждом следующем
рекурсивном обращении, образует рекурсивный стек.
7. Рекурсивные методы решения задач нашли широкое применение
в процедурном программировании.
вычисление через .
3. Исполнитель умеет выполнять два действия: "+1", "*2".
Составьте программу получения из числа 1 числа 100 за наименьшее
количество операций.
4. Найдите наибольший общий делитель двух натуральных чисел с
помощью алгоритма Евклида.
5. Дано натуральное число, кратное 3. Получите сумму кубов этого
числа, затем сумму кубов получившегося числа и т.д. Проверьте на
нескольких примерах, действительно ли в конечном итоге получится 153.
6. Разработайте программу вычисления an натуральной
степени n вещественного числа a за наименьшее число операций.
Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
Титульный лист.
Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
Выводы по лабораторной работе.
Ответы на контрольные вопросы.
Контрольные вопросы
Приведите примеры рекурсивных объектов и явлений. Обоснуйте
проявление рекурсивности.
Почему при правильной организации рекурсивные вызовы не
зацикливаются?
Почему не отождествляются совпадающие идентификаторы при
многократных рекурсивных вызовах?
Почему рекурсивные обращения завершаются в порядке,
обратном вызовам этих обращений?
Чем ограничено при выполнении программы количество
рекурсивных вызовов?
Какой из методов в программировании является более
эффективным – рекурсивный или итерационный?
Лабораторная работа № 8. Примеры использования рекурсивных и
итеративных алгоритмов
Теоретическая часть
Одной из идей процедурного программирования, которая оформилась в
начале шестидесятых годов ХХ века, стало активное применение в практике
программирования некоторого метода, основанного на организации серий
взаимных обращений программ (функций) друг к другу. Вопросы об
эффективности использования данного метода при
разработке алгоритмических моделей актуальны и в настоящее время,
несмотря на существование различных парадигм программирования,
создание новых и совершенствование существующих языков
программирования. Речь идет о рекурсивном методе в программировании,
который рассматривается альтернативным по отношению к итерационному.
begin
writeln (Max(Mas,0,9))
{l=0, r=9 т.к. параметр A в списке формальных параметров
функции Max – открытый массив, т.е. значения индексов начинаются
от 0}
end
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.
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
#include "stdafx.h"
#include <iostream>
using namespace std;
#define max 20
void centr(int n,float *x, float *y, float *c);
Ключевые термины
База рекурсии – это тривиальный случай, при котором решение задачи
очевидно, то есть не требуется обращение функции к себе.
Глубина рекурсивных вызовов – это наибольшее одновременное
количество рекурсивных обращений функции, определяющее максимальное
количество слоев рекурсивного стека.
Декомпозиция – это выражение общего случая через более простые
подзадачи с измененными параметрами.
Корень полного дерева рекурсивных вызовов – это вершина полного
дерева рекурсии, соответствующая начальному обращению к функции.
Косвенная (взаимная) рекурсия – это последовательность взаимных
вызовов нескольких функций, организованная в виде
циклического замыкания на тело первоначальной функции, но с иным
набором параметров.
Объем рекурсии - это характеристика сложности рекурсивных
вычислений для конкретного набора параметров, представляющая собой
количество вершин полного рекурсивного дерева без единицы.
Параметризация – это выделение из постановки задачи параметров,
которые используются для описания условия задачи и решения.
Полное дерево рекурсии – это граф, вершинами которого являются
наборы фактических параметров при всех вызовах функции, начиная с
первого обращения к ней, а ребрами – пары таких наборов, соответствующих
взаимным вызовам.
Прямая рекурсия – это непосредственное обращение рекурсивной
функции к себе, но с иным набором входных данных.
Рекурсивная триада – это этапы решения задач рекурсивным
методом.
Рекурсивная функция – это функция, которая в своем теле содержит
обращение к самой себе с измененным набором параметров.
Рекурсивный алгоритм – это алгоритм, в определении которого
содержится прямой или косвенный вызов этого же алгоритма.
Рекурсия – это определение объекта посредством ссылки на себя.
Краткие итоги
1. Рекурсия характеризуется определением объекта посредством
ссылки на себя.
2. Рекурсивные алгоритмы содержат в своем теле прямое или
опосредованное обращение с самим себе.
3. Рекурсивные функции содержат в своем теле обращение к самим
себе с измененным набором параметров в виде прямой рекурсии. При этом
обращение к себе может быть организовано посредством косвенной
рекурсии – через цепочку взаимных обращений функций, замыкающихся в
итоге на первоначальную функцию.
4. Решение задач рекурсивными способами проводится
посредством разработки рекурсивной триады.
5. Целесообразность применения рекурсии в программировании
обусловлена спецификой задач, в постановке которых явно или опосредовано
указывается на возможность сведения задачи к подзадачам, аналогичным
самой задаче.
6. Рекурсивные методы решения задач широко используются при
моделировании задач из различных предметных областей.
7. Рекурсивные алгоритмы относятся к ресурсоемким алгоритмам.
Для оценки сложности рекурсивных алгоритмов учитывается число вершин
полного рекурсивного дерева, количество передаваемых параметров,
временные затраты на организацию стековых слоев.
Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
Титульный лист.
Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
Выводы по лабораторной работе.
Ответы на контрольные вопросы.
Контрольные вопросы
Приведите примеры рекурсивных объектов и явлений. Обоснуйте
проявление рекурсивности.
Почему при правильной организации рекурсивные вызовы не
зацикливаются?
Почему не отождествляются совпадающие идентификаторы при
многократных рекурсивных вызовах?
Почему рекурсивные обращения завершаются в порядке,
обратном вызовам этих обращений?
Чем ограничено при выполнении программы количество
рекурсивных вызовов?
Какой из методов в программировании является более
эффективным – рекурсивный или итерационный?
Можно ли случай косвенной рекурсии свести к прямой рекурсии?
Ответ обоснуйте.
Может ли рекурсивная база содержать несколько тривиальных
случаев? Ответ обоснуйте.
Являются ли параметры, база и декомпозиция единственными для
конкретной задачи? Ответ обоснуйте.
С какой целью в задачах происходит пересмотр или
корректировка выбранных параметров, выделенной базы или
случая декомпозиции?
Является ли рекурсия универсальным способом решения задач?
Ответ обоснуйте.
Почему для оценки трудоемкости рекурсивного алгоритма
недостаточно одного метода подсчета вершин рекурсивного дерева?
Лабораторная работа № 9. Ознакомление 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 целых чисел в вектор и с
помощью перегруженного оператора индексации [] получаем к ним
доступ для их последующего вывода:
Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
Титульный лист.
Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
Выводы по лабораторной работе.
Ответы на контрольные вопросы.
Контрольные вопросы:
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 для
этой цели использовались макроопределения с параметрами. Недостатки
такого подхода в том, что препроцессор, подставляя вместо вызова
макроопределения его текст, ничего не проверяет, что является источником
многих ошибок. Обнаружить эти ошибки непросто, поскольку программист
видит только вызов макроопределения, а не тот текст, который получается
после его разворачивания.
template<список_шаблонных_параметров_через_запятую>
{
тело_класса
};
class Stack
T data[s];
int c;
public:
Stack() { c = 0; }
T pop()
};
Теперь, чтобы создать какой-нибудь стек, нужно написать примерно
следующее:
Stack<int, 200> S;
Результат:
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, параметром которого служит
итератор, указывающий на удаляемый элемент. Последний метод
возвращает другой итератор, указывающий на следующий за удаляемым
элемент.
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 этот класс и
сделан вложенным.
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) для каждой из ваших программ (проектов)
отдельно.
Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
Титульный лист.
Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
Выводы по лабораторной работе.
Ответы на контрольные вопросы.
Контрольные вопросы:
16.Что из себя представляют шаблонные классы?
17.Синтаксис шаблонного класса
18.Опишите готовые шаблонные классы для стека (файл stack),
очереди (файл queue), вектор (файл vector), связный (более точно,
двусвязный) список
19.Что такое итератор?
20.Опишите итераторы ввода.
21.Опишите Однонаправленные итераторы
22.Опишите Двунаправленные итераторы
23.Опишите итераторы произвольного доступа
24.Приведите пример использования итератора
Лабораторная работа № 11. Реализация классов. Конструкторы и
деструкторы классов. Перегрузка и переопределение методов
Цель: ознакомится с реализацией классов, изучить конструкторы и
деструкторы классов.
Теоретическая часть
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;
};
В вашей программе вы просто определяете конструктор так же, как
любой другой метод класса:
#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);
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;};
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. Когда вы введете оклад,
программа отобразит информацию об обоих служащих.
Конструктор копирования
Конструктор копирования создает объект класса, копируя при этом
данные из уже существующего объекта данного класса.
Деструктор
Деструктор (от 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 – сработает конструктор по
умолчанию.
class BitSet
{
private:
int *bits;
public:
//проверка – установлен ли бит в множестве
bool isSet(int i);
//конструктор класса битовых множеств
BitSet();
//деструктор класса битовых множеств
~BitSet();
};
struct ExceptionBitSet{
int index;
int ErrorCode;
ExceptionBitSet(int _index, int _ErrorCode):
index(_index), ErrorCode(_ErrorCode){}
};
#endif
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;
}
Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
Титульный лист.
Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
Выводы по лабораторной работе.
Ответы на контрольные вопросы.
Контрольные вопросы:
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"; }
};
int main()
{
Derv1 dv1; //Объект производного класса 1
Derv2 dv2; //Объект производного класса 2
Base* ptr; //Указатель на базовый класс
int main()
{
Derv1 dv1; //Объект производного класса 1
Derv2 dv2; //Объект производного класса 2
Base* ptr; //Указатель на базовый класс
int main()
{
// Base bad; //невозможно создать объект
//из абстрактного класса
Base* arr[2]; //массив указателей на
//базовый класс
Derv1 dv1; //Объект производного класса 1
Derv2 dv2; //Объект производного класса 2
Дружественные функции
Принцип инкапсуляции и ограничения доступа к данным запрещает
функциям, не являющимися методами соответствующего класса, доступ к
скрытым (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 main()
{
alpha aa;
beta bb;
cout << frifunc(aa, bb) << endl; //вызов функции
return 0;
}
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;
}
Требования к отчету.
Отчет по лабораторной работе должен соответствовать следующей
структуре.
• Титульный лист.
• Словесная постановка задачи. В этом подразделе проводится
полное описание задачи. Описывается суть задачи, анализ входящих в нее
физических величин, область их допустимых значений, единицы их
измерения, возможные ограничения, анализ условий при которых задача
имеет решение (не имеет решения), анализ ожидаемых результатов.
• Математическая модель. В этом подразделе вводятся
математические описания физических величин и математическое описание
их взаимодействий. Цель подраздела – представить решаемую задачу в
математической формулировке.
• Алгоритм решения задачи. В подразделе описывается разработка
структуры алгоритма, обосновывается абстракция данных, задача
разбивается на подзадачи.
• Листинг программы. Подраздел должен содержать текст
программы на языке программирования.
• Контрольный тест. Подраздел содержит наборы исходных
данных и полученные в ходе выполнения программы результаты.
• Выводы по лабораторной работе.
• Ответы на контрольные вопросы.
Контрольные вопросы:
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 появляется в:
а) классе, разрешающем доступ к другому классу;
б) классе, требующем доступа к другому классу;
в) разделе скрытых компонентов класса;
г) разделе общедоступных компонентов класса.