Тема 4.4. Структуры данных, шаблоны и методы 4.4.webp
Перегрузка операторов

Перегрузка оператора присваивания =

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

Разница между конструктором копирования и оператором присваивания:

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

Пример:

441_01

Создан объект s1 и для него выделена динамическая память под массив. Затем так же отдельно создается объект s2. Но после выполнения инструкции s2 = s1; (т.к. копирование выполняется побитово) объекты s1 и s2 будут хранить адрес одного и того же массива по одному адресу. Потом включается деструктор, очищает память после объекта s2, пытается очистить память после объекта s1, а часть этой памяти (выделенная под массив) оказывается уже очищенной.

Можно использовать следующую конструкцию:

441_02

Здесь объект s2 еще не создан и для его конструирования сразу запускается конструктор копирования.

Использование перегрузки с помощью функции operator =:

441_03

В строке 25: если указатель не null, т.е. если вдруг указатель хранит адрес выделенной для массива памяти, то очистить ту память (чтоб не было утечки), здесь if вернет true.

441_04

В 49 строке не сработает конструктор копирования, потому что была описана перегрузка оператора присваивания – она и сработает.

Всё корректно работает:

441_05

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

  • int x = 1;
  • int y = 0;
  • int z = 2;
  • z = y = x; // у всех будет значение 1

Но применительно к объектам такой синтаксис просто так не сработает:

441_06

Но если сделать возврат значения:

441_07

Тогда всё скомпилируется:

441_08

Перегрузка операторов сравнения 

441_09

Сравнение объектов – это сравнение содержимого объектов.

Просто так сравнить объекты не получится:

441_10

441_11

Использование перегрузки с помощью функций operator == и operator !=

Пример:

441_12

В функции сравнивается содержимое объектов – значения всех свойств.

this – это cat1, а в oldCat передается ссылка на объект cat2. Функция перегрузки вызывается в 32 и 33 строках в момент использования == и !=.

По такому же принципу выполняется перегрузка операторов

  • >
  • <
  • >=
  • <=

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

Перегрузка арифметических операторов

441_13

Суммирование объектов – это суммирование содержимого объектов.

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

441_14

Перегрузка арифметических операторов работает по тому же принципу, что и перегрузка операторов сравнения:

441_15

По такому же принципу выполняется перегрузка операторов

  • *
  • /
  • %

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

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

Перегрузка операторов инкремента и декремента (префиксной и постфиксной формы)

Просто так выполнить операцию инкремента (или декремента) для объекта не получится:

441_16

Префиксная форма:

441_17

Постфиксная форма:

441_18

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

Перегрузка оператора индексирования []

Обычный способ обращения к свойству:

441_19

Без перегрузки через объект обратиться к элементу массива не получится:

441_20

Если в разделе public описана перегрузка оператора [], тогда можно через объект обратиться напрямую к элементу массива, который (массив) является свойством класса:

441_21

Из строки 19 индекс [1] передается в качестве параметра в функцию перегрузки и записывается в параметр index. Далее строка 9 возвращает значение по ссылке на этот элемент в строку 19. Если значение возвращать по ссылке, то с помощью перегрузки можно также и записывать новое значение.

Если убрать & из 7 строки, то будет возвращаться не ссылка на хранящееся в массиве значение (на число 20), а копия значения (копия числа 20):

441_22

Следовательно, изменения в массиве происходить не могут, и компилятор протестует.

Тем не менее просто вывести эту копию с помощью cout всё еще возможно.

Такой манёвр не удастся из-за строк 12 – 15:

441_23

Перегрузка оператора (). Функтор

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

441_24


Работа с исключениями

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

Пример:

442_01

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

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

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

1. Выполнить по образцу.

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

442_02

Если num2 равен нулю, то бросить “Деление на ноль недопустимо” в параметр oshibka в 26 строке. В 28 строке вывести содержимое oshibka.

2. Выполнить по образцу.

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

442_03

Здесь тоже с помощью catch перехватывается ошибка и передается способ ее обработки. Оператор принимает параметр типа int, чтобы использовать его значение для вывода информации о том, в чем заключается ошибка.

Исключения могут быть выброшены в любом месте кода: для этого нужно прописать throw и указать, что именно нужно “бросить” в catch.

3. Выполнить по образцу.

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

442_04

442_05

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

При возникновении исключения (throw) C++ копирует сгенерированный объект в некоторое нейтральное место. После этого просматривается конец текущего блока try. Если блок try в данной функции не найден, управление передается вызывающей функции, где и осуществляется поиск обработчика. Если и здесь не найден блок try, поиск повторяется далее, вверх по стеку вызывающих функций. Этот процесс и называется разворачиванием стека. На каждом этапе разворачивания стека все объекты, которые выходя из области видимости, уничтожаются так же, как если бы функция выполнила команду return. Это оберегает программу от потери ресурсов и “праздно шатающихся” неуничтоженных объектов.

Когда необходимый блок try найден, программа ищет первый блок catch, который должен находиться сразу после закрывающей скобки блока try. Если тип сгенерированного объекта совпадает с типом аргумента, указанным в блоке catch, управление передается этому блоку; если же нет, проверяется следующий блок catch. Если в результате подходящий блок не найден, программа продолжает поиск уровнем выше, пока не будет обнаружен необходимый блок catch. Если искомый блок не обнаружен, программа аварийно завершается.

4. Выполнить по образцу.

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

442_06

В одной программе может быть несколько блоков try, catch, throw. При выполнении блока throw будет производиться поиск подходящего (по параметрам в скобках) блока catch (если такой блок не будет найден, то программа аварийно завершит свою работу). В примере сработает блок catch из 26 строки.

5. Выполнить по образцу.

Написать следующий программный код:

442_07

Протестируйте работу программы:

442_08

442_09

442_10

Работа с файлами

Потоки для работы с файлами создаются как объекты следующих классов:

  • ofstream – класс для вывода (записи) данных в файл
  • ifstream – класс для ввода (чтения) данных из файла
  • fstream – класс для чтения и для записи данных (двунаправленный обмен)

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

443_01

1. Выполнить по образцу.

Метод open()

Написать следующий программный код:

443_02

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

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

Можно создать и использовать строковую переменную, в которую записать путь или имя файла:

443_03

Можно получить значение этой переменной от пользователя (через консоль):

443_04

2. Выполнить по образцу.

Метод is_open()

Написать следующий программный код:

443_05

Стандартный метод is_open() возвращает true, если удалось открыть файл, или возвращает false, если файл открыть не удалось.

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

443_06

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

443_07

Если в одной программе несколько записей (10 и 11 строки), то данные будут дописываться в файл:

443_08

Или можно так с тем же результатом:

443_09

3. Выполнить по образцу.

Метод close()

Написать следующий программный код:

443_10

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

Параметр ofstream::app (где app – это статическая переменная, означающая append (англ.) – добавить) необходим для того, чтобы указать на необходимость дописывания новых данных к уже существующим данным без их предварительного удаления. В файле file5.txt уже хранилось “12345”.

Дописать следующий код:

443_11

Дописать следующий код (для пользовательского ввода):

443_12

4. Получить от пользователя массив данных типа double, записать их в файл.

5. Выполнить по образцу.

Чтение данных из файла

Написать следующий программный код:

443_13

Чтение данных из файла может осуществляться побайтово (посимвольно) или построчно.

Условие в цикле: пока в файле есть символы, метод get() возвращает true, считывая и выводя из файла в консоль очередной символ: сначала ‘1’, потом ‘2’, потом ‘3’, потом ‘\n’; потом ‘2’ и т.д.

Так же посимвольно возможно считывание данных любого типа:

443_14

6. Выполнить по образцу.

Метод eof()

Написать следующий программный код:

443_15

Метод eof() (end of file) возвращает значение false до тех пор, пока не будет достигнут конец файла. Оператор >> в строке 16 будет считывать данные до первого пробельного символа.

Попробовать в 17 строке не использовать endl.

Получится:

443_16

Внезапный баг при использовании eof():

443_17

Если в самом текстовом файле в конце последнего символа нажать Enter, то последним элементом в файле будет “\n”. После того как 17 строка кода выведет “лишней.”, метод eof() обнаружит, что там еще не конец файла – там еще что-то есть. Значит начнется новая итерация цикла. В 16 строке в stroka пробельные символы не записываются, значит в stroka всё ещё будет храниться “лишней.”, которая и выведется с помощью той же 17 строки кода.

Попробовать способ решения:

443_18

Чтобы организовать построчное считывание, нужно использовать метод getline() из библиотеки (метод getline() считывает строку до первого Enter’а):

443_19

7. Создать на рабочем столе файл формата .txt, написать в нем небольшой текст, переместить файл в simba\\:обмен файлами\001. Затем создать проект, скопировать из simba файл другого студента в папку со своим проектом. Вывести в консоль содержимое файла.

8. Выполнить по образцу.

Запись объекта в файл

Написать следующий программный код:

443_20

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

9. Выполнить по образцу.

Чтение объекта из файла

Написать следующий программный код:

443_21

443_22

Нажать F5, чтобы запустить отладку.

10. Описать класс по заданной предметной области (3-5 свойств). Создать массив из нескольких объектов. Записать значения для свойств объектов (или получить данные от пользователя). Выполнить запись данных об объектах в один файл. Выполнить вывод данных об объектах в консоль.

11. Выполнить по образцу.

Класс fstream

Написать следующий программный код:

443_23

443_24

fstream (сокращение от «FileStream») — заголовочный файл из стандартной библиотеки C++, включающий набор классов, методов и функций, которые предоставляют интерфейс для чтения/записи данных из/в файл. Для манипуляции с данными файлов используются объекты, называемые потоками (“stream”).

Протестировать работу программы:

443_25

443_26

443_27

443_28

Открыть файл, чтобы посмотреть результат:

443_29

12. Выполнить по образцу.

Перегрузка оператора <<

Написать следующий программный код:

443_30

443_31

Открыть файл, чтобы посмотреть результат.

13. Выполнить по образцу.

Перегрузка оператора >>

Написать следующий программный код:

443_32

В папке с проектом уже был файл myFile.txt с данными. Нужно эти данные из файла вывести в консоль.

Открыть файл, чтобы посмотреть результат.

Шаблоны классов, умные указатели

Шаблон класса

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

Шаблон класса (class template) позволяет задать тип для объектов, используемых в классе. Принцип работы такой же, как и у шаблонных функций:

template 

или

template  

Пример:

444_01

В шаблонном классе Saving могут содержаться как свойства типа T, так и обычные свойства, например, double balance. При создании объекта необходимо в <> передавать тип данных для T, например, .Метод SizeOfType просто выводит размер переменной accountNum переданного ранее типа.

Кроме того, можно так же как и в шаблонных функциях, назначить T типом возвращаемого значения. Можно передавать в T такой тип данных как имя класса. Можно создать несколько типов: T1, T2, T3 и т.д. и передавать в параметрах несколько разнотипных значений:

template 

Saving saving1 (25, “q1w2”);

Saving saving2 (25, 30);

Наследование шаблонных классов

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

typeid(double) – это объект. Через него идет обращение к функции:

444_02

Если функция не определяется, то нужно подключить библиотеку #include 

Пример:

444_03

Базовый шаблонный класс Saving, шаблонный подкласс PersonalSaving. В 17 строке при указании базового класса нужно указать и имя типа для шаблона (). При конструировании объекта сначала вызывается 17 строка. Значение из (acNum) копируется в (T value), и тип T становится int. В этой же 17 строке указано, что есть наследование, и что следует обратиться к конструктору базового класса Saving(value). Т.е. сначала должна конструироваться часть из базового класса. В базовом классе в 8 строку конструктор принимает из 20 строки значение 25 из (value). Выполняет 10 строку. Затем конструируется часть из подкласса (там пусто). Далее 37 строка вызывает метод ShowTypeName(), в нем через оператор typeid() идет обращение к стандартному методу name(), который возвращает тип данных аргумента – Saving ::accountNum.

Так тоже сработает:

444_04

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

444_05

В этой программе описан шаблонный класс Saving (строки 4-12), который может принимать для своего метода Display() любой тип данных в параметр value.

Также описана специализация для шаблона класса (строки 14-22): если объект создавался с типом, например, string, то для этого объекта сработает метод Display() из специализированного шаблона.

Умные указатели

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

Например, следующими ошибками:

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

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

Пример:

444_06

444_07

Указатель auto_ptr

Пример:

444_08

Если создать второй объект (38 строка) и выполнить операцию “=” из объекта sp1, то на самом деле в результате этого второй объект будет указывать на ту же область памяти, что и первый объект.

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

444_09

Чтобы этого не произошло, используется указатель auto_ptr.

Вместо этого:

444_10

нужно написать так:

444_11

и подключить библиотеку:

444_12

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

Указатель unique_ptr

С unique_ptr  такой маневр не удастся:

444_13

Чтобы сменить владельца данных для unique_ptr, нужно использовать функцию move из пространства имен std: нужно присвоить результат работы этой функции в новую переменную:

444_14

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

Вместо move можно использовать так же работающую функцию из стандартной библиотеки swap():

444_15

Указатель shared_ptr

shared_ptr в отличие от unique_ptr может одновременно хранить в двух указателях ссылки на один и тот же участок памяти.

444_16

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

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

Умный указатель shared_ptr считается самым удобным и поэтому наиболее используемым из умных указателей.

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

Пример:

444_17

Благодаря умному указателю shared_ptr после выхода массива из области видимости память из-под него очистится автоматически.

Вектор

Вектор – шаблон из стандартной библиотеки C++, реализующий динамический массив. Фактически это замена стандартному динамическому массиву (память для которого выделяется вручную с помощью оператора new). Использование векторов позволяет избежать утечек памяти и облегчает работу программисту.

Пример:

445_01

Для вывода или изменения значения элемента можно обратиться к нему по индексу (так же как и при работе с обычным массивом). Обращение к нескольким элементам вектора осуществляется так же как и обращение к нескольким элементам массива – с использованием цикла for:

445_02

Возможна инициализация вектора так же как и массива – через {}.

Если выводить вектор по индексам с помощью [], то не будет проверяться, был ли выход за границы вектора: программа может крашнуться во время выполнения, может не крашнуться:

445_03

Функция at()

Можно выводить элементы вектора с помощью стандартной функции at():

445_04

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

445_05

Вывод через at() будет работать медленнее, чем вывод через [], как раз из-за проверки на выход за границы вектора.

Функция push_back()

Стандартная функция push_back() применяется для добавления элементов в вектор.

Пример 1:

445_06

Пример 2:

445_07

Функция pop_back()

Стандартная функция pop_back() удаляет последний элемент вектора:

445_08

Функция clear()

Стандартная функция clear() удаляет все элементы вектора:

445_09

Функции size() и empty()

С помощью функции size() можно узнать размер вектора, а с помощью функции empty() можно проверить, пустой ли вектор:

445_10

Функцию size() удобно использовать во втором параметре цикла for:

445_11

Проверка на пустоту: если вектор пуст, то empty() вернет true (1), если не пуст, то вернет false (0).

Пример 1:

445_12

Пример 2:

445_13

Функция capacity()

Пример:

445_14

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

445_15

В 9 строке значение (5) будет указывать кол-во реальных элементов и для size(), и для capacity(). При этом все элементы будут инициализированы нулем. Если нужно инициализировать все элементы другим числом, то нужно указать это число в качестве второго параметра:

445_16

Функция reserve()

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

445_17

Функция shrink_to_fit()

Если известно, что в какой-то момент вектор больше дополняться не будет, то можно сэкономить память, использовав стандартную функцию shrink_to_fit(), которая ужмёт вектор до реального размера: в этой программе была выделена новая память для массива из 8 элементов, туда были скопированы эти элементы, а память из-под старого массива была возвращена в кучу:

445_18

Функция resize()

Изменение размера вектора с помощью стандартной функции resize():

445_19

Ключевое слово typedef

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

445_20

Односвязный список, итераторы

Односвязный список

Линейный однонаправленный список — это структура данных, состоящая из элементов одного типа, связанных между собой последовательно посредством указателей. Каждый элемент списка имеет указатель на следующий элемент. Последний элемент списка указывает на NULL. Элемент, на который нет указателя, является первым (головным) элементом списка. Здесь ссылка в каждом узле указывает на следующий узел в списке. В односвязном списке можно передвигаться только в сторону конца списка. Узнать адрес предыдущего элемента, опираясь на содержимое текущего узла, невозможно. Получить напрямую элемент (по индексу как в массиве) нельзя. Список располагается в памяти не по порядку, как массив, поэтому для добавления/удаления элемента не нужно создавать новый список и копировать туда элементы, как это делается с массивом.

446_01

Пример:

446_02


446_03


446_04


Результат:

446_05


Или можно рандомно заполнить список, получив от пользователя размер списка:

446_06


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

446_07


446_08


Немного подкорректированная main():

446_09


Результат (удалён первый хэд):

446_10


В конечном итоге нужно будет удалить весь список. Для этого надо добавить метод clear():

446_11


446_12


main():

446_13


Результат:

446_14


Логичнее разместить вызов метода clear() в деструкторе: когда будет выход из main(), удалится список:

446_15


main():

446_16


Результат:

446_17


Метод push_front() для добавления элементов в начало списка:

446_18


446_19


Метод insert() для добавления элемента по нужному индексу:

446_20


446_21


main():

446_22


Метод removeAt() для удаления элемента по нужному индексу:

446_23


446_24


main():

446_25


Метод pop_back() для удаления последнего элемента:

446_26


446_27


main():

446_28


Цикл for each

Цикл for each – это цикл на основе диапазона. Он предназначен для тех случаев, когда требуется перебрать все элементы в массиве, векторе, списке и т.п.

446_29


В строке 12 в скобках: из массива myArray будут по одному браться значения элементов массива (с начала и до конца массива), копируется (по значению) в переменную value и для каждого значения будет выполняться тело цикла.

Можно передавать значения по ссылке:

446_30


Аналогичный пример со списком:

446_31


Итератор

Итератор – это такая структура данных, которая используется для обращения к определенному элементу в контейнерах STL. С помощью итераторов удобно перебирать элементы. Итератор описывается типом iterator. Но для каждого контейнера конкретный тип итератора будет отличаться. Обычно итераторы используются с контейнерами set, list, а у вектора для этого применяются индексы.

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

Пример:

446_32


С помощью арифметики указателей через итератор можно получить доступ к последующим элементам вектора:

446_33


Стандартная функция end() “указывает” в конец последнего элемента.

446_34


Если сделать итератор константным, то через вектор всё равно можно изменить значение элемента:

446_35


Если сделать итератор константным, то через итератор (константный итератор myIterator) нельзя изменить значение элемента:

446_36


Можно использовать reverse_iterator, чтобы получить элементы в обратном порядке. Тогда функция rbegin() будет “указывать” в конец и брать элементы, начиная с конца вектора, а rend(), наоборот, будет “указывать” в начало:

446_37


Функция advance() позволяет сдвинуть итератор на указанное число позиций (та же самая операция, что и в арифметике указателей):

446_38


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

Функция insert() позволяет добавить элемент в начало:

446_39


Сместив итератор, можно вставить нужное значение в любую позицию вектора:

446_40


или так:

446_41


Функция erase() позволяет удалить первый элемент:

446_42


Сместив итератор, можно удалить нужное значение из любой позиции вектора:

446_43


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

446_44


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

446_45


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

446_46


Создание перечисления

Перечисления в языке C++ прямо наследуют поведение перечислений языка C, за исключением того, что перечисляемый тип в C++ — настоящий тип, и ключевое слово enum используется только при объявлении такого типа. Если при обработке параметра являющегося перечислением, какое-либо значение из перечисления не обрабатывается (например один из элементов перечисления забыли обработать в конструкции switch), то компилятор может выдать предупреждение о забытом значении.

1. Выполнить по образцу.

Написать следующий программный код:

447_01

В 4 строке помощью ключевого слова enum создано перечисление. В {} для первого элемента можно указать любое число, с которого нужно начинать перечисление, иначе по умолчанию перечисление начнется с нуля. Теперь в kasha хранится 1, в sup2, в salat3. В 10 строке в переменную для пользовательского ввода numDinner записывается 1. Далее в конструкции switch реализуется выбор. С помощью enum т.о. можно избежать использования “магических чисел” в программе и повысить самодокументируемость кода. На самом деле в 21, 25 и 29 строках сравниваются числа – 1, 2 и 3, соответствующие элементам перечисления.

Нажать F5, чтобы запустить отладку:

447_02

2. В папке 001 найти и скопировать файл enum Planet.exe в папку D:\bin\Student, запустить. Написать код этой программы, используя перечисление. Текстовый вывод информации можно сократить до названия планеты.

3. Выполнить по образцу.

Написать следующий программный код:

447_03

Как использовать перечисления в программе с классами: создано перечисление в 4 строке; описан класс Светофор с геттером и сеттером. В классе есть закрытое свойство color. Тип возвращаемого значения геттераперечисление, т.е. вернется что-то из списка – красный, желтый или зеленый. Сеттер принимает параметр типа перечисление Color (т.е. принимает цвет) и записывает его в свойство color для текущего объекта. В main() создается объект s класса Светофор. В 29 строке через этот объект вызывается сеттер и ему передается в качестве параметра элемент red из перечисления Color. Далее можно дописать все ветки условий. В ветке if вызывается через объект s геттер и будет возвращен тот цвет, который там сейчас хранится – red. Тут же он будет сравниваться == с элементом red перечисления Color. Если получится true, то сработает 33 строка.

В этой программе логично будет расположить перечисление Color внутри класса Svetofor. При обращении к Color нужно будет теперь указывать имя класса Svetofor::Color.

Переписать код следующим образом:

447_04

Если элементы перечисления должны хранить какие-то содержательные значения, а не просто 0, 1, 2 и т.д., то можно необходимые значения им присвоить:

447_05

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

447_06

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

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

Идентификатор, определённый в пространстве имён, ассоциируется с этим пространством. Один и тот же идентификатор может быть независимо определён в нескольких пространствах. Таким образом, значение, связанное с идентификатором, определённым в одном пространстве имён, может иметь (или не иметь) такое же значение, как и такой же идентификатор, определённый в другом пространстве. В пространстве имен std содержатся все компоненты стандартной библиотеки C++, например: cout, iostream.

1. Выполнить по образцу.

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

Написать следующий программный код:

448_01

Нажать F5 для запуска отладки.

2. Выполнить по образцу.

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

Написать следующий программный код:

448_02

Нажать F5 для запуска отладки.

Если один и тот же разработчик описывает обе части Car, то он расположит их внутри одного блока namespace Car (переписать код):

448_03

Нажать F5 для запуска отладки.

Если подключить (3 строка) описанное разработчиком пространство имен, то в программе можно будет не указывать каждый раз Car:: (переписать код):

448_04

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

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

Переписать код:

448_05

Нажать F5 для запуска отладки.

Тренажёр