Объектно-ориентированное программирование на языке C++
Оценка 5

Объектно-ориентированное программирование на языке C++

Оценка 5
Домашнее обучение +1
pdf
информатика
10 кл—11 кл
18.07.2023
Объектно-ориентированное программирование на языке C++
Базовое ООП на С++
Пособие Давыденко.pdf

Оглавление

1.  Знакомство с ООП ................................................................................................................................... 2

1.1     Классы ................................................................................................................................................ 2

1.2     Инкапсуляция .................................................................................................................................... 3

1.3     Наследование .................................................................................................................................... 3

1.4     Полиморфизм ................................................................................................................................... 4

2.  Введение в классы .................................................................................................................................. 5

2.1     Создание класса и объекта класса .................................................................................................. 5

2.2     Методы класса .................................................................................................................................. 7

2.3     Модификаторы доступа .................................................................................................................. 9

2.4     Конструктор и деструктор .............................................................................................................. 12

2.5     Конструктор копирования ............................................................................................................. 16

2.6     Ключевое слово this ....................................................................................................................... 17

2.7     Статические поля класса ................................................................................................................ 19

2.8     Статические методы класса ........................................................................................................... 21

2.9     Дружественные методы класса .................................................................................................... 22

2.10  Дружественные классы................................................................................................................ 24

2.11  Перегрузка операторов ................................................................................................................ 25

2.12  Задания для закрепления знаний из главы 2 ........................................................................... 31

3.  Наследование ........................................................................................................................................ 32

3.1     Модификаторы доступа при наследовании ............................................................................... 35

3.2     Конструкторы и деструкторы в наследовании ........................................................................... 37

3.3     Виртуальные функции ................................................................................................................... 38

3.4     Абстрактные классы ....................................................................................................................... 41

3.5     Множественное наследование ..................................................................................................... 43

3.6     Конструкторы и деструкторы в множественном наследовании .............................................. 45

3.7     Виртуальное наследование ........................................................................................................... 46

3.8     Вложенные классы ......................................................................................................................... 48

3.9     Задания для закрепления знаний из главы 3 ............................................................................. 49

4.  Шаблоны ................................................................................................................................................ 51

4.1     Шаблоны классов ........................................................................................................................... 51

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

Контрольное задание ............................................................................................................................... 56

Приложение А ............................................................................................................................................ 58

 

 

1. Введение в ООП

Эволюция разработки программного обеспечения все время шла таким образом, чтобы облегчить восприятие данных, с которыми работает код. И объектно-ориентированный подход возник для именно того, чтобы упростить программисту работу с кодом. Сегодня эта тема актуальна как никогда, в любом языке программирования важно реализовывать больше функций, используя меньше кода, с этим и помогает ООП. В программном коде реализация объектов осуществляется с помощью классов. И, говоря очень простым языком, ООП позволяет осуществлять различные действия с классами. Но возникает вопрос – что такое классы? 

 

1.1 Классы

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

У вас в игре есть лучник, маг и воин. Все три персонажа являются объектом класса «Герой». Класс «Герой» обозначает некий шаблон, который имеет какие-то базовые свойства, к примеру здоровье или урон. Так вот, один наш экземпляр героя, пусть это будет лучник, является объектом класса «Герой» и у этого экземпляра есть свои свойства или же члены, которые созданы по шаблону, и есть какая-то функция – идти, в ООП такие функции называются методами. Точно также, маг и воин являются отдельными объектами одного и того же класса «Герой», но имеют свои свойства и функции.

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

 

1.2 Инкапсуляция

Инкапсуляция – это принцип независимости данных в программировании. То есть, при разработке класса можно сделать много разных свойств, которые необходимы для работы объекта, и скрыть их от пользователя для прямого редактирования, и сделать методы, с помощью которых он сможет работать с этим объектом. За примером снова обратимся к машинам:

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

 

1.3 Наследование

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

Представим наших лучника, мага и воина не как объекты класса «Герой», а как отдельные классы, которые были наследованы от него. Очевидно, что раз это разные классы, то у них должны быть отличия, так вот в качестве таких отличий обозначим для лучника наличие лука, для мага наличие «маны», а для воина возможность использовать щит. Поскольку все эти классы наследники класса «Герой», то они имеют общие свойства, например урон и здоровье, но их значения для каждого героя будут разные, и общие методы, например ходить. При создании класса наследника «Воин» он перенимает все свойства и методы класса «Герой» и добавляет свои – свойство «Щит» и методы «Поднять» и «Опустить». Таким образом, класс «Воин», так же как «Маг» и «Лучник» имеет одинаковые свойства урон и здоровье, которые указаны в классе «Герой», но и получил дополнительное конкретно для себя – щит, а также два новых метода – поднять и опустить. 

 

1.4 Полиморфизм

Полиморфизм – возможность объекта вести себя по-разному в зависимости от ситуации и реагировать на какое-то одно действие, но какимто специфическим образом именно для этого класса объектов. И снова герои:

К примеру, нашим героям нужно атаковать, но у каждого оружие будет разным. Для каждого героя метод атаковать будет одинаковым, который указан в классе «Герой», но, в зависимости от типа героя, будь то маг или лучник, метод атаковать будет выполняться по-разному. Это и есть полиморфизм – выполнение одной функции разными способами.

                 

2. Введение в классы

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

 

2.1 Создание класса и объекта класса

Класс – это пользовательский тип данных, то есть тип, который вы можете создать сами на основе стандартных, таких как int, double и прочие.

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

class Hero //объявление класса Auto

{

//

//тело класса

//

};

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

Hero warrior;

После этого мы можем использовать эту переменную, как если бы, например, использовали переменную int a;.Но, что мы можем сделать с этой переменной на данный момент? Ничего. Пока что, мы указали лишь название файла без свойств или методов, наш класс не умеет хранить даже какие-то данные. Все это должно быть в теле класса. Для создания какого-то параметра(свойства) можно использовать стандартные типы данных, в качестве примера добавим несколько параметров в наш класс Auto, пусть это будет скорость и цвет:

class Hero //объявление класса Hero

{ public:

                double hp; //параметр здоровья  string race; //параметр расы

};

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

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

warrior.hp; //обращение к полю класса

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

 

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

После выбора поля нужно бы присвоить ему значение, делается это с помощью обычного оператора присваивания «=».

warrior.race = "Человек"; //присваивание значения полю класса

Присвоим нашим полям здоровья и расы значения 120 и Человек. После этого у нас есть типа данных «Hero», который комбинирует в себе сразу несколько отдельных типов данных. Для добавления каких-то новых полей можно просто дописать его в тело класса, и оно автоматически добавится в класс и будет готово для использования. 

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

Задания для самопроверки

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

 

2.2 Методы класса

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

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

class Hero 

{ public:

                   double hp; 

                     string race; 

 

                              void Info() // объявление функции Info

                 {

                 }

};

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

void Info() 

{

                cout << "Раса - " << race << ",\tЗдоровье - " << hp << "оч." << endl; }

Вызвать ее можно так же, как поле класса, после точки.  warrior.Info();

После использования метода Info() в консоль выведется информация о автомобиле. Если использовать метод для другого объекта нашего класса, например archer, он выведет информацию конкретно об этом объекте и его полях. 

Также, определять логику методов класса можно за классом. Для этого в самом классе нужно объявить метод:

void info();

И чтобы написать его функционал за классом используется следующая запись:

                      void Hero::info()

                 {

                                                           cout << "Раса - " << race << ",\tЗдоровье - " << hp << "оч." << endl;

}

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

Задание для самопроверки

2.2.1 Возьмите класс из задания 2.1.1 и напишите несколько методов, выводящих информацию об объекте в разном виде.

 

2.3 Модификаторы доступа

Модификаторы доступа используются в коде для того, чтобы оградить его от ненужного вмешательства. В пункте 2.1 мы уже упоминали один из модификаторов доступа – public. Помимо него существуют еще private и protected. Работают они следующим образом:

class Hero { public:

/*

                     Область Public

*/ private:

/*

                     Область Private

*/

};

Эти области доступа отвечают за то, будут ли видны поля в них за пределами определения класса напрямую, если не указывать никакие модификаторы доступа, то все поля, определенные в классе, по умолчанию будут приватными. В предыдущих примерах мы получали значения полей напрямую, вызывая их через точку, но это было возможно потому, что они были в области Public, поля из этой области могут быть использованы в любом другом месте кода без лишних манипуляций. Поля в области Private не будут доступны для прямого редактирования, они попросту будут скрыты от обычного пользователя и доступны для взаимодействия лишь внутри определения класса. Рассмотрим на примере:

class Hero { public:              double hp; // Доступно везде private:

                             string race; //Доступно внутри класса

};

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

hero.race;

В начале был упомянут еще один модификатор – protected. На данный момент он будет работать аналогично public, скрывая поле от внешнего использования, но в главе, в которой мы будем говорить про наследование, мы вернемся к этому модификатору и увидим, для чего же он нужен.

Все вышеперечисленное также относится к методам класса, которые тоже можно делать публичными и приватными:

class Hero { public:

                double hp;            void Race_Info()

                 {

                                              cout << "Раса - " << race << endl;

                                    Hp_Info();

                 }

private:

                     string race; 

                      void Hp_Info()

                 {

                                                cout << "Hp - " << hp << "оч." << endl;

                 }

};

Разберем пример поподробнее. Добавилось два новых метода: Race_Info и Hp_Info, которые отвечают за вывод информации о цвете и скорости. Race_Info хоть и находится в публичной области, выводит информацию о поле из приватной, так как, повторюсь, любые поля и методы, определенные в private, доступны для работы внутри области класса. Также в этом методе вызывается функция Hp_Info, которая находится в приватной области. Просто так вызвать такую функцию через точку мы не сможем, но, если она находится в определении публичного метода, то при его вызове отработает и наша приватная функция. Таким образом, мы смогли вывести приватную переменную и использовать приватный метод за пределами класса.

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

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

Здесь можно поподробнее остановиться на принципе инкапсуляции. В ООП не принято предоставлять доступ к полям напрямую и поэтому для работы с полями, обычно, создаются методы доступа к этим полям, часто их называют Get(получить) и Set(установить). Делают их для того, чтобы неопытный пользователь не мог нарушить работу программы, использовав поля класса не так, как было задумано. Эти методы образуют интерфейс взаимодействия между кодом и пользователем, позволяя закладывать в свойства необходимую нам логику. Рассмотрим пример:

class Hero 

{ public:                 double getHp() //Метод получения значения

                 {

                                    return hp;

                 }

 

                                void setHp(double my_hp) // Метод установки значения

                 {

                                   hp = my_hp;

                 }

 

                       string getRace()

                 {

                                     return race;

                 }

 

                           void setColor(string my_race)

                 {

                                     race = my_race;

                 }

private:

                   double hp; 

                     string race; 

};

Мы объявили приватные переменные и публичные методы для взаимодействия с ними. Методы getHp и getRace возвращают значение поля, ничего сложного. Методы setHp и setRace передают значение, введенное пользователем, в нашу переменную. Важно, чтобы «геттеры», они же Get функции, были такого же типа, как и поля, для которых они были созданы. 

Итак, теперь, чтобы задать значение поля и получить его обратно используется следующая логика:

                warrior.setHp(100); //Задаем значение Hp  warrior.getHp(); // Получаем значение Hp

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

Задание для самопроверки

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

 

2.4 Конструктор и деструктор

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

class Hero { public:

 Hero(double my_hp, string my_race) //Первый конструктор, получающий значения

                 {

                                   hp = my_hp;

                                     race = my_race;

                 }

 

                           Hero() //Второй конструктор по умолчанию

                 {

                                   hp = 0;

                                        race = "Отсутствует";

                 }

private:

                double hp;            string race;

};

Поговорим подробнее про каждый конструктор. Первый конструктор получает в себя два значения, которые в теле конструктора присваиваются указанным скрытым полям, после чего объект инициализируется в памяти. Второй конструктор называется конструктором по умолчанию, он не имеет аргументов и нужен для того, чтобы объявить объект без инициализации, присвоив ему сразу какие-то начальные значения, заданные в теле конструктора. Обычно, такой конструктор компилятор делает сам, даже если мы его не указали, и заполняет поля «мусором», но, если мы создаем свой собственный конструктор, то в таком случае конструктор по умолчанию нужно объявить самостоятельно.

Посмотрим, как использовать конструкторы:

Hero warrior(100, "Человек");

Здесь используется первый конструктор, который после вызова передает в поле hp значение 100, а в поле race значение “Человек”.

Hero archer;

Тут уже используется конструктор по умолчанию и после создания объекта в его поля поместятся значения 0 и “Отсутствует”, которые мы указали в теле конструктора.

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

class Hero { public:

                Hero(double my_hp, string my_race) : hp{ my_hp }, race{ my_race } {}                Hero() : hp{ 0 }, race{ "Отсутсвует" } {} private:

                   double hp;

                     string race;

};

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

Стоит также поговорить про делегирующие, для начала рассмотрим пример, когда в классе присутствует несколько конструкторов:

class Hero { public:

                      Hero(string my_race)

                 {

                                race = my_race;                    hp = 0;

                                    level = 0;

                 }

 

                            Hero(string my_race, int my_level)

                 {

                                race = my_race;                    level = my_level;                              hp = 0;

                 }

 

                                Hero(string my_race, int my_level, double my_hp)

                 {

                                race = my_race;                    level = my_level;

                                  hp = my_hp;

                 }

 

                  Hero()

                 {

                                  hp = 0;

                                       race = "Отсутствует";

                                      level = my_level;

                 }

 private:

                double hp;             string race;

                     int level;

};

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

class Hero { public:

                          Hero(string my_race) : Hero()

                 {

                                    race = my_race;

                 }

 

                                 Hero(string my_race, int my_level) : Hero(my_race)

                 {

                                      level = my_level;

                 }

 

                                         Hero(string my_race, int my_level, double my_hp) : Hero(my_race, my_level)

                 {

                                  hp = my_hp;

                 }

 

                  Hero()

                 {

                                  hp = 0;

                                race = "Отсутствует";                          level = 0; 

                 }

 private:

                double hp;             string race;

                     int level;

};

Для    вызова        конструктором    другого      конструктора используется следующая запись:

Hero(string my_race, int my_level) : Hero(my_race)

После объявления конструктора ставится двоеточие, и вызывается другой конструктор, который уже существует, и в него передается нужное поле. Таким образом Hero(string my_race, int my_level) этот конструктор сначала вызовет этот Hero(my_race) , в нем инициализируются «базовые» поля, после чего вызывающий конструктор изменит поля согласно полученным аргументам.

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

void exmp()

{//начало области видимости

                   Hero a;

}//конец области видимости

Деструктор, как и конструктор по умолчанию, компилятор создает автоматически, даже если его не написать, но определять его явным образом нужно лишь тогда, когда в конструкторе происходит захват какого-то объекта, к примеру, динамической памяти. Определяется деструктор также, как конструктор, но перед названием класса необходимо указать тильду(~).

Важно! Деструктор не принимает никакие параметры.

class Hero { private:            double* p = new double; public:

                       Hero() //конструктор

                 {

 

                 }

 

                      ~Hero()//деструктор

                 {

                                    delete p;

                 }

};

 

Задание для самопроверки

2.4.1 Модифицируйте свой класс, добавив в него конструктор для всех полей, конструктор по умолчанию и конструктор, принимающий не все поля. Сначала напишите конструкторы без делегации, а потом «улучшите» код с их помощью.

 

2.5 Конструктор копирования

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

int a = 5; int b = a;

Здесь, после объявления переменной b она будет равна 5. Также можно сделать и с объектами нашего класса, но только до тех пор, пока поля класса не будут содержать ссылок или указателей. В обратном случае, при попытке использовать конструктор копирования он создаст объект, который будет указывать на ту же область памяти, что и объект, с которого он был скопирован. Из-за этого, при вызове деструктора, сперва удалится копия и область памяти, в которой она находилась, после чего деструктор для оригинала просто не сможет понять, что ему нужно удалять и произойдет ошибка.

Объявляется конструктор копирования следующим образом:

class Hero { private:

                double* p = new double;    double* b = new double; public:

                                   Hero(const Hero& a) : p{ a.p }, b{ a.b } {}

};

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

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

Hero(const Hero& a) = delete;

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

 

2.6 Ключевое слово this

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

Hero(double my_hp, string my_race) //Первый конструктор, получающий значения

                 {

                                   hp = my_hp;

                                     race = my_race;

                 }

Фактически, здесь уже используется ключевое слово this, только неявно, как это делается с конструктором по умолчанию. У полей hp и race есть свои адреса памяти, по которым система к ним обращается и эти адреса уникальны для каждого объекта класса.

 

Рассмотрим пример применения this:

Hero(double hp, string race

                 {

                                     this->hp = hp;

                                       this->race = race;

}

Первое, что изменилось, это изменения передаваемых в конструктор локальных переменных, они стали идентичны названиям полей. И это не является ошибкой, потому что второе, что изменилось - появилось ключевое слово this. Оно указывает на поле конкретного объекта класса, создаваемое этим конструктором и именно поэтому аналогичные названия локальных переменных не являются ошибкой, так как при использовании this они не занесут свои значения в самих себя, а передадут их полю класс. 

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

Hero &level_up()

                 {

                                this->level += 1;                    this->hp += 10;

                                     return *this;

}

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

warrior.level_up().level_up();

Она аналогична такой записи:

warrior.level_up();               warrior.level_up();

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

Задания для самопроверки

2.6.1 Измените конструкторы из задания 2.4.1, используя ключевое слово this и убедитесь, что они работают как прежде.

 

2.7 Статические поля класса

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

class Hero { public:

                        static int hero_count;

                                Hero(double hp, string race, int level)

                 {

                                this->hp = hp;                        this->race = race;                  this->level = level;

                                   hero_count ++;

                 }

 private:

                int level;                string race;

                   double hp;

}; 

int Hero:: hero_count = 0;

 

int main() {

                         setlocale(LC_ALL, "ru");

                 

                           Hero warrior(150, "Человек", 1);

                          Hero archer(100, "Эльф", 1);

                        Hero mage(75, "Гном", 1);

 

                cout << Hero::hero_count << endl;  return 0;

}

В данном примере добавлена статическая переменная hero_count объявляется в теле класса, а определяется за классом. 

                          int Hero:: hero_count = 0;

 

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

cout << Hero::hero_count << endl;

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

cout << archer.hero_count << endl;

Но, в обоих случая вывод будет один и тот же, из-за чего принято использовать первую запись.

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

2.8 Статические методы класса

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

class Hero { public:

                                Hero(double hp, string race, int level)

                 {

                                this->hp = hp;                        this->race = race;                  this->level = level;

                                   hero_count++;

                 }

 

                         static int Get_Count()

                 {

                                     return hero_count;

                 }

 private:

                int level;                string race;  double hp;

                        static int hero_count;

};

Вызывается статический метод аналогично статической переменной:

Hero::Get_Count()

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

                           static void level_up(Hero& hero)

                 {

                                    hero.level++;

                 }

Вызывается он следующим образом:

Hero::level_up(mage);

 

Задания для самопроверки

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

 

2.9 Дружественные методы класса

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

friend тип_функции имя_класса_друга::название_функции();

Рассмотрим применение дружественных функций на примере:

class Hero; class Enemy;

 

class Enemy

{ public:

                    Enemy(double a)

                 {

                                     this->damage = a;

                 }

 

                 Enemy()

                 {

                                     this->damage = 0;

                 }

 

                        void attack(Hero& hero);

 private:

                   double damage;

}; 

class Hero { public:

                                Hero(double hp, string race, int level)

                 {

                                this->hp = hp;                        this->race = race;

                                         this->level = level;

                 }

 

                  Hero() 

                 {

                                this->hp = 0;                          this->race = "None";

                                       this->level = 0;

                 }

                             friend void Enemy::attack(Hero& hero);

 

                     void info()

                 {

                                      cout << this->hp;

                 }

private:

                int level;                string race;

                   double hp;

}; 

void Enemy::attack(Hero& hero)

{

                        hero.hp -= this->damage;

}

Прежде всего были объявлены имена классов:

class Hero; class Enemy;

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

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

void attack(Hero& hero);

В качестве аргумента передается объект класса герой, которому будет нанесен урон. После того, как метод объявлен, он делается дружественным для другого класса:

friend void Enemy::attack(Hero& hero);

Здесь метод просто был объявлен, важно, чтобы сигнатура метода совпадала с той, которая определена в классе врага. Определен он будет позже, за классом. Делается это из-за того, что при вызове этот метод использует объекты класса Герой и в этот момент они все должны быть известны.

                         void Enemy::attack(Hero& hero)

                 {

                                       hero.hp -= this->damage;

                 }

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

                    Enemy golem(10);

                Hero mage(100, "Human", 1); golem.attack(mage);

2.10 Дружественные классы

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

friend Enemy;

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

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

Задания для самопроверки

2.10.1 Создайте еще один класс и сделайте некоторые его методы дружественными для вашего основного класса. 

 

2.11 Перегрузка операторов

Так как наш класс по сути своей является новым типом данных, как int, double и прочие, то у него могут быть свои арифметические, и не только, операции, которые можно перегрузить и заложить в них логику, необходимую именно нам. Надеюсь, ранее вы были знакомы со структурами в языке C++, и знаете, что в них перегрузка операторов оформляется за структурой. В классах же чаще всего перегрузка оператора определяется как метод класса. Как пример, рассмотрим перегрузку оператора сложения для нашего класса Hero, конечно, в данном контексте она будет слегка бессмысленна, но главное познакомиться с синтаксисом перегрузок операторов в классах. 

class Hero { private:

                double hp;             int level;  string race;

                           string default_race = "Человек";

 public:

                                Hero(double hp, string race, int level)

                 {

                                this->hp = hp;                        this->race = race;

                                         this->level = level;

                 }

 

                          Hero(double hp, int level)

                 {

                                this->hp = hp;                        this->race = default_race;

                                         this->level = level;

                 }

 

                  Hero()

                 {

                                     this->hp = 0;

                                          this->race = "Отсутствует";

                                       this->level = 0;

                 }

                             Hero operator + (const Hero& b) const;

 

                     void Info()

                 {

  cout << "Раса - " << race << ",\tЗдоровье - " << hp << "оч. Уровень - " << level <<  endl;

                 }

 

                double get_hp() const { return hp; }  int get_level() const { return level; }

 

};

 

Hero Hero::operator + (const Hero& b) const

{

                                 return { hp + b.hp, level + b.level };

int main() {

                setlocale(LC_ALL, "ru");       Hero warrior(100, "Человек", 1);

                          Hero warrior2(150, "Орк", 3);

 

                           warrior2 = warrior2 + warrior;

 

                      warrior2.Info();

                   return 0;

}

 

Для начала оператор объявляется в классе:

Hero operator + (const Hero& b) const;

После этого за классом определяется его логика:

                               Hero Hero::operator + (const Hero& b) const

                 {

                                                 return { hp + b.hp, level + b.level };

                 }

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

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

warrior2 = warrior2 + warrior;

Воспринимается компилятором следующим образом:

warrior2 = warrior2.operator+(warrior);

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

В итоге, после выполнения сложения warrior2 получит силу и здоровье warrior’а.

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

Hero operator + (const Hero& a, const Hero& b)

{

                return { a.get_hp() + b.get_hp(), a.get_level() + b.get_level()}; }

В данном случае в качестве аргументов указываются два объекта класса, а доступ к скрытым полям осуществляется через «геттеры» и «сеттеры». Этот способ используют, когда невозможен второй, когда операция вызывается не объектом класса, а, например, числовым типом int или double, тогда

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

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

Hero& operator += (const Hero& b); // объявление в классе

                                  Hero& Hero::operator += (const Hero& b) //объявление за классом

                 {

                                hp += b.hp;                            level += b.level;

                                     return *this;

}

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

bool operator == (const Hero& b) const;

                               bool Hero::operator == (const Hero& b) const

                 {

                                                    return hp == b.hp && level == b.level && race == b.race;

}

 

1.                 Префиксная форма инкермента – операнд должен сначала измениться и лишь после этого участвовать в выражении.

Hero& operator ++ ();

                        Hero& Hero::operator ++ ()

                 {

                                level++;                  hp++;

                                     return *this;

}

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

Hero operator ++ (int);

                          Hero Hero::operator ++ (int)

                 {

                                Hero a = *this;                       level++;

                                   return a;

}

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

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

По требованию синтаксиса операция должна вернуть ссылку на вызываемый объект, иначе не будут работать цепочки присваивания по типу a = b = c.

Hero& operator = (const Hero& b);

                            Hero& Hero::operator = (const Hero& b)

                 {

                                if (&b == this) return *this;                                 race = b.race;                  level = b.level;                      hp = b.hp;

                                     return *this;

}

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

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

void operator = (const Hero&) = delete; // запрет оператора копирования

Также стоит рассмотреть перегрузку операторов ввода-вывода. Операторы ввода-вывода это стандартные cin и cout(>> и <<), но на данный момент они не умеют работать с объектами класса. Для того, чтобы это исправить их нужно перегрузить, но поскольку операторы ввода-вывода являются частью библиотеки fstream, поэтому перегрузка будет отличаться от перегрузки простых операторов. Во-первых, для работы с библиотекой fstream нужно подключить ее с помощью include, во-вторых, перегрузка происходит сразу за классом и не определятся внутри его, и в таком случае, чтобы получать доступ к скрытым полям класса нужно сделать перегрузку оператора дружественной:

friend ostream& operator<<(ostream& o, const Hero& hero);

В качестве аргумента передается ссылка на объект нашего класса, после чего, сохраняя сигнатуру, перегрузка определяется за классом:

ostream& operator<<(ostream& o, const Hero& hero)

{

                                      o << hero.race << " " << hero.level << " " << hero.hp;

                   return o;

}

После этого мы сможем использовать оператор << для вывода данных на консоль, как если бы это был обычный тип int или double, по функционалу он схож с нашей функцией info, которая выводит информацию об объекте, но помимо работы с консолью, появилась возможность заносить объекты класса в файл:

                    ofstream os;

                              os.open("File.txt", ofstream::app);

                Hero hero(5, 150, "Гном");                Hero hero1(10, 200, "Эльф");

                        if (!os.is_open())

                 {

                                            cout << "Ошибка при открытии файла!" << endl;

                 }

                 else

                 {

                                       os << hero1 << "\n";

                                       os << hero << "\n";

                 }

os.close();

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

istream& operator>>(istream& i, Hero& hero)

{

 i >> hero.race >> hero.level >> hero.hp;  return i;

}

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

                ifstream is;             is.open("File.txt");               Hero hero2;

                        if (!is.is_open())

                 {

                                            cout << "Ошибка при открытии файла!" << endl;

                 }

                 else

                 {

                                    while (true)

                                {

                                                is >> hero2;                             if (is.eof())

                                                {

                                                                 break;

                                                }

                                                       cout << hero2 << endl;

                                }

                 }

                                     is.close();

 

Задания для самопроверки

2.11.1 Попробуйте самостоятельно написать перегрузку для «-=», «--» и «!=». Также, используйте классы, созданные в ходе выполнения предыдущих заданий и сделайте для них перегрузку потокового ввода-вывода, записав информацию об объекте в файл.

 

2.12 Задания для закрепления знаний из главы 2

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

2.12.2. Создайте класс "Массив", который содержит свойства "размер" и "элементы". Добавьте методы, позволяющие задать и получить размер массива, а также получить значение элемента по индексу. Также добавьте методы для сортировки и поиска элементов в массиве.

 

3. Наследование

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

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

Ранее в примерах использовались герои «Воин», «Лучник» и «Маг», все они были объектами одного класса «Герой» и у них были общие поля, но, представим, что у каждого из героев есть свои особенности, у Воина это будет параметр Силы, у Лучника параметр Ловкости, а у Мага параметр Интеллекта. Для этого каждому из героев пришлось бы делать отдельный класс и добавлять в него индивидуальный параметр:

class Warrior

{ public:

                double hp;             string race;  int level;

                     int strange;

}; 

class Archer

{ public:

                double hp;             string race;  int level;

                      int agility;

}; 

class Mage { public:

                double hp;             string race;  int level;

                       int intelligence;

};

Сразу можно увидеть, что у всех трех классов есть по три общих поля –

Здоровье, Уровень и Раса, в реальности таких одинаковых полей может быть сколь угодно много и для каждого класса придется описывать эти поля отдельно, помимо этого у каждого класса могут быть одинаковые методы, которые придется копировать в каждый новый класс, а если еще нужно будет этот метод изменить, то придется делать это отдельно в каждом классе. Конечно, это все не очень удобно и практично и может привести к ошибкам. Для избежания всех этих лишних действий придумано наследование, то есть мы можем сделать эти три класса наследником, например, класса Герой, который будет иметь все общие поля и методы, и наследуемые классы также будут иметь доступ к этим полям и методам.

class Hero { public:

                double hp;             string race;

                     int level;

}; 

class Warrior : public Hero

{ public:

                     int strange;

}; 

class Archer : public Hero

{ public:

                      int agility;

}; 

class Mage : public Hero

{ public:

                       int intelligence;

};

В данном примере добавился класс Hero, который называется базовым и от него наследованы производные классы наших Воинов, Лучников и Магов.

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

class имя_класса : модификатор_доступа имя_базового_класса

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

                 

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

 

Видим, что объект класса лучник имеет доступ к полям класса герой.

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

class Mage : public Hero

{ public:

                       int intelligence;

                    void beat()

                 {

                                         cout << "Vzhuhh" << endl;;

                 }

}; 

class Warrior_Mage : public Mage

{ public:

                    int manapool;

 

                    void beat()

                 {

                                           cout << "Vzhuhh-dzinn" << endl;;

                 }

};

Класс «Воин-Маг» был унаследован от класса «Маг» и получил его методы и поля, а также поля от класса «Герой», так как класс «Маг» был унаследован от него, при этом в оба класса добавлены одинаковые методы «Удар», но которые отработают по-разному, в зависимости от того, какой класс его вызвал. 

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

Задания для самопроверки

3.0.1 Возьмите класс «Студент» из задания 2.12.1 и создайте на его основе классы «Бакалавр» и «Магистр», добавив при этом каждому полю уникальные поля и методы.

 

3.1 Модификаторы доступа при наследовании

В первой главе уже были затронуты модификаторы доступа и было сказано, что существует их три – public, private и protected. Поговорим о них подробнее в контексте наследования. 

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

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

 

Если говорить простыми словами, модификатор доступа при наследовании изменяет модификаторы доступа в базовом классе конкретно для наследника. Разберем работу модификаторов на примере класса example:

class example

{ public:

                int a; private:

                int b; protected:

                   int c;

};

Использовании public при наследовании: class heir : public example

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

Использование private при наследовании: class heir : private example

В таком случае все модификаторы доступа класса example для класса heir становятся private, то есть использовать их нельзя ни в классе hair, ни объектом класса heir.

Использование protected при наследовании: class heir : protected example

В этом случае в классе example для класса heir изменится только модификатор public, он станет protected, то есть будет доступен для использования только в классе heir.

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

 

3.2 Конструкторы и деструкторы в наследовании

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

class example

{ public:

                   int a;

                   double b;

                        example(int a, double b)

                 {

                                this->a = a;                             this->b = b;

                 }

                  example()

                 {

 

                 }

}; 

class heir : public example

{ public:

                   int c;

                                  heir(int a, double b, int c) : example(a,b)

                 {

                                     this->c = c;

                 }

}; 

class heir_heir : public heir

{ public:

                   int d;

 

                                           heir_heir(int a, double b, int c, int d) : heir(a, b, c)

                 {

                                     this->d = d;

                 }

};

Класс example является базовым, от него наследуется класс heir, а от этого класса наследуется класс heir_heir. В каждом из них есть свои конструкторы, но в них определяется лишь одно поле, которое уникально для наследуемого класса. При создании объекта наследуемого класса сначала вызывается конструктор базового класса, которые определяется следующим образом:

heir(int a, double b, int c) : example(a,b)

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

heir_heir(int a, double b, int c, int d) : heir(a, b, c)

Сначала компилятор обратится к конструктору класса heir, увидит, что тот ссылается на конструктор базового класса example и начнет «сборку» объекта с базового конструктора, постепенно дойдя до конструктора текущего класса. 

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

Задания для самопроверки

3.2.1 Добавьте для классов из задания 3.0.1 конструкторы, используя наследование.

3.3 Виртуальные функции

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

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

class Player

{ public:

                      void beat(Hero hero)

                 {

                                        cout << "Beat" << endl;

                 }

 

                        void beat(Warrior hero)

                 {

                                        cout << "Bzzn" << endl;

                 }

 

                       void beat(Archer hero)

                 {

                                        cout << "Brrrn" << endl;

                 }

 

                      void beat(Mage hero)

                 {

                                        cout << "Vzhuuh" << endl;

                 }

};

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

Синтаксис у таких функций следующий:

virtual тип_функции имя_функции()

Рассмотрим, как их использовать:

class Hero { public:

                double hp;             string race;

                     int level;

 

                        virtual void beat()

                 {

                                        cout << "Beat" << endl;

                 }

}; 

class Warrior : public Hero

{ public:

                 

                       void beat() override

                 {

                                        cout << "Bzzn" << endl;

                 }

};

 

class Archer : public Hero

{ public:

                       void beat() override

                 {

                                        cout << "Brrrn" << endl;

                 }

}; 

class Mage : public Hero

{ public:

                       void beat() override

                 {

                                        cout << "Vzhuuh" << endl;

                 }

};

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

Итак, в примере добавлен метод beat, который для каждого класса наследника работает по-разному. Но как это использовать? Очевидно, что можно вызывать методы через точку, но виртуальные функции придуманы не для этого. Чтобы показать достоинства использования полиморфизма, изменим класс игрока, который будет использовать возможности виртуальных функций. Вместо описания методов атаки у игрока для каждого героя можно в качестве аргумента передавать указатель на объект класса и вызывать у этого объекта метод атаки, в таком случае компилятор сам поймет, какой именно метод нужно использовать.

class Player

{ public:

                       void beat(Hero* hero)

                 {

                                    hero->beat();

                 }

};

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

 int main() {

                         setlocale(LC_ALL, "ru");

 

                   Hero hero;

                    Warrior war;

                    Archer arch;

                  Mage mage;

                     Player player;

 

                player.beat(&mage);           player.beat(&arch);  player.beat(&war);

                      player.beat(&hero);

                   return 0;

}

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

Задания для самопроверки

3.3.1 Добавьте для классов из задания 3.2.1 метод, который будет отображать «работу» студента, сделав для каждого класса разную реализацию этого метода.

3.4 Абстрактные классы

Сейчас в примерах классы «типов» героев наследуются от класса герой, который тоже является отдельным типом героя. Часто существует нужда создать какой-то класс, который описывал бы общие характеристики, в нашем случае персонажа, и уже от которого можно было бы более тонко настраивать наследников этого класса. Для таких целей используются абстрактные классы - это классы, объекты которых невозможно создать и они служат базовым классом для его наследников. Для того, чтобы класс считался абстрактным, в нем должна быть как минимум одна чисто виртуальная функция — это такая функция, которая не имеет в себе реализации и служит «каркасом» для того, чтобы ее переопределили в классах наследниках. Чисто виртуальные функции объявляются следующим образом:

virtual тип_функции имя_функции() = 0;

Создадим класс «Персонаж» для нашего примера, который будет абстрактным классом и будет служить базовым классом для создания персонажей.

class Character

{ public:

                double hp;             string race;

                     int level;

                           virtual void beat() = 0;

}; 

class Hero : public Character

{ public:

                        virtual void beat()

                 {

                                        cout << "Beat" << endl;

                 }

}; 

class Warrior : public Character

{ public:

                 

                       void beat() override

                 {

                                        cout << "Bzzn" << endl;

                 }

}; 

class Archer : public Character

{ public:

                       void beat() override

                 {

                                        cout << "Brrrn" << endl;

                 }

}; 

class Mage : public Character

{ public:

                       void beat() override

                 {

                                        cout << "Vzhuuh" << endl;

                 }

};

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

Теперь изменим класс игрок и в качестве аргумента метод атаки будет получать указатель на объект абстрактного класса, но так как наши типы героев являются наследником этого класса, ошибок возникать не будет.

class Player

{ public:

                           void beat(Character* character)

                 {

                                      character->beat();

                 }

};

Абстрактные классы удобно использовать, когда известно, что у классов наследников будет много общих полей и методов, и, чтобы уменьшить количество кода используются подобные «общие» классы.

Задания для самопроверки

3.4.1 Сделайте класс «Студент» из задания 3.3.1 абстрактным классом и наследуйте от него классы «Бакалавр», «Магистр», «Аспирант».

 

3.5 Множественное наследование

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

class Warrior

{ public:

                int strength;          void attack()

                 {

                                        cout << "Bzzn" << endl;

                 }

}; 

class Mage { public:

                       int intelligence;

                     void attack()

                 {

                                        cout << "Vzhuuh" << endl;

                 }

}; 

class MageWarrior : public Warrior, public Mage

{ public:

                     void attack()

                 {

                                           cout << "Bzzn - Vzhuuh" << endl;

                 }

};

При создании класса воин-маг используется следующая запись:

class MageWarrior : public Warrior, public Mage

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

Теперь, если попытаться вызвать метод атаки у объекта класса ВоинМаг, то вызовется тот метод, который был определен в нем. Но, если нужно применить атаку какого-то конкретного класса, нужно перед вызовом функции привести объект Мага-Воина к классу, у которого нужно вызвать метод атаки:

((Warrior)mw).attack();

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

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

Задания для самопроверки

3.5.1 Создайте класс «Магистрант-Аспирант» и «Бакалавр-Магистрант», используя множественное наследование классов из задания 3.4.1.

 

3.6 Конструкторы и деструкторы в множественном наследовании

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

class MageWarrior : public Warrior, public Mage

Сначала записан класс Воина, потом класс Мага, в таком случае компилятор сначала вызовет конструктор класса Воин, затем конструктор класса Маг и в последнюю очередь конструктор класса Воин-Маг. То есть порядок вызова конструкторов зависит от того, в каком порядке были указаны базовые классы при объявлении класса наследника. Конечно, в данном примере не имеет значения, какой конструктор будет вызываться за каким, но, в реальных задачах, например, могут присутствовать некоторые зависимости, которые нужно будет учитывать при порядке определения родителей. 

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

Задания для самопроверки

3.6.1 Сделайте для новых классов из задания 3.5.1 конструкторы.

 

 

 

 

3.7 Виртуальное наследование

Для лучшего понимая этой темы обратимся к примеру из пункта 3.5. Как уже было сказано ранее, наши герои являются наследниками одного класса Персонаж, поэтому изменим этот пример:

class Character

{ public:

                         virtual void attack() 

                 {

                                         cout << "Attack" << endl;

                 }

}; 

class Warrior : public Character {};

 

class Mage : public Character{};

 

class MageWarrior : public Warrior, public Mage{};

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

 

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

class Warrior : public virtual Character {}; class Mage : public virtual Character{};

 

После этого компилятор воспринимает создание класса Маг-Воин следующим образом:

 

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

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

Задания для самопроверки

3.7.1 Исправьте наследование классов из задания 3.5.1, добавив им виртуальное наследование. 

3.8 Вложенные классы

Вложенные классы – это классы, которые созданы внутри других классов. Чаще всего такие классы используют для каких-то внутренних нужд класса, который его содержит. Для понимания, как устроено определение внутреннего класса, рассмотрим пример:

class external

{ public:

                     void info()

                 {

                                       abc.attached_info();

                 }

private:

                     class attached

                 {

                public:                    attached(int a, int b)

                                {

                                                this->a = a;                                             this->b = b;

                                }

                                   attached()

                                {

                                                this->a = 0;                                            this->b = 0;

                                }

                                       void attached_info()

                                {

                                                                       cout << "Объект вложенного класса: a = " << a << ", b = " << b

<< endl;

                                }

                   private:

                                int a;                       int b;

                 };

                        attached abc{ 1,2 };

};

Для начала был создан класс, в который будет вложен другой – external, такие классы еще называют объемлющими, и вложенный класс – attached. Определение вложенного класса ничем не отличается от обычного, в нем также объявляются поля, конструкторы и методы. Для примера во вложенном классе просто были объявлены поля a и b и метод, выводящий информацию об объекте. Теперь, чтобы использовать этот класс необходимо в области видимости объемлющего класса создать объект вложенного класса, но для того, чтобы задать поля, используются фигурные скобки. Все, теперь, например, в методах объемлющего класса можно использовать такой объект и все его методы. Как пример, создадим объект класса external и получим информацию об объекте вложенного класса.

int main() {

 setlocale(LC_ALL, "ru");  external a;

 

                    a.info();

 

 

                   return 0;

}

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

external::attached abc(1, 2);

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

Задания для самопроверки

3.8.1 Создайте в одном из классов наследников вложенный класс, который будет хранить информацию об успеваемости студентов, например, класс «Зачетка».

 

3.9 Задания для закрепления знаний из главы 3

3.9.1. Создайте класс "Фигура", который будет являться базовым классом для других классов геометрических фигур, таких как круг, прямоугольник, ромб и треугольник. В классе "Фигура" определите методы для вычисления площади и периметра фигуры.

3.9.2 Создайте абстрактный класс "Животное" с несколькими полями по вашему выбору и виртуальной функцией "Голос". Создайте классы "Собака" и “Кошка”, которые наследуются от класса "Животное" и реализуют виртуальную функцию "Голос" для вывода на экран строки, соответствующей голосу кошки или собаки. Определите конструкторы классов и создайте объект наследуемых и вызовите их методы.

                 

4. Шаблоны

 

4.1 Шаблоны классов

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

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

template<typename T> class tmpclass

{ private:

                T exmp1; public:

                     tmpclass(T exmp1)

                 {

                                     this->exmp1 = exmp1;

                 }

                     void t_info()

                 {

                                                  cout << "Тип поля - " << typeid(exmp1).name() << endl;

                 }

};

Для начала перед классом указывается ключевое слово template после чего в угловых скобках указывается слово typename, которое означает, что в

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

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

tmpclass<char> a('a');

После имени класса в угловых скобочках указывается типа данных, с которыми этому классу нужно работать в самом классе и наш тип данных T будет считаться тем типом, который указан в скобочках. И после этого можно вызвать метод t_info, который выведет тип поля. Аналогично с другими типами данных, например double:

tmpclass<double> a(3.14);

И в таком случае метод t_info выведет уже double, то есть типа заданной

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

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

template<typename T1, typename T2> class tmpclass

{ private:

                  T1 exmp1;

                T2 exmp2; public:

                        tmpclass(T1 exmp1, T2 exmp2)

                 {

                                this->exmp1 = exmp1;                         this->exmp2 = exmp2;

                 }

                     void t_info()

                 {

                                cout << "Тип первого поля - " << typeid(exmp1).name() << ", тип второго поля - " <<typeid(exmp2).name() << endl;

                 }

};

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

tmpclass<double, int> a(3.14, 10);

 

Также у шаблонных классов присутствует возможность специализации, своего рода перегрузки для шаблонов. Хотя заранее и неизвестно, с каким именно типом данных будет работать класс, его работу можно настроить для каждого из типов. В момент, когда создается объект шаблонного класса, компилятор сам как бы создает класс, который работает с тем типом, который был указан при создании, поэтому существует возможность самостоятельно создать такой класс и компилятор будет понимать какой именно «шаблон» ему использовать. Для того, чтобы специализировать шаблон используется следующая запись:

                  template<>

class tmpclass<double>

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

template<>

class tmpclass<double>

{ private:

                double exmp1; public:

                      tmpclass(double exmp1)

                 {

                                     this->exmp1 = exmp1;

                 }

                     void t_info()

                 {

  cout << "Тип поля - " << typeid(exmp1).name() << " - это тип дробных чисел" << endl;

                 }

};

Теперь, когда будет создаваться объект шаблонного класса типа double, компилятор будет обращаться не к стандартному шаблонному классу, а к этому, который был специализирован. Если создать объект типа int:

                                       tmpclass<int> a(10);

                                     a.t_info();

То на экран выведется сообщения из стандартного шаблонного класса:

«Тип поля – int»

Но если создать объект класса double, то компилятор будет обращаться к специализированному шаблону и выведет следующее сообщение:

«Тип поля - double - это тип дробных чисел»

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

Задания для самопроверки

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

 

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

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

class tmp : public tmpclass<int, double>

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

                template<typename T1, typename T2> class tmp : public tmpclass<T1, T2>

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

Рассмотрим небольшой пример унаследованного класса:

template<typename T1, typename T2> class tmp : public tmpclass<T1, T2>

{ public:

                              tmp(T1 a, T2 b) : tmpclass(a, b){}

 

                     void print()

                 {

                                cout << "Первое поле - " << exmp1 << ", второе поле - " << exmp2 << endl;

                 }

};

Этот класс получил метод вывода переменных на экран. Для того, чтобы в функции print использовать поля базового класса, в самом базовом классе необходимо изменить их модификатор доступа с public на protected. 

Задания для самопроверки

4.2.1 Создайте класс наследник от классов из задания 4.1.1 и протестируйте его возможности.

 

4.3 Задания для закрепления знаний из главы 4

4.3.1 Создайте шаблон класса "Стек", который содержит стек элементов указанного типа данных. Реализуйте методы добавления элемента на вершину стека, удаления элемента с вершины стека и получения размера стека, а также вывода этого списка.

 

 

                 

Контрольные задания

Задание 1

Создайте класс "Календарь", который содержит список событий на определенный период времени. Добавьте методы, позволяющие добавлять, удалять и редактировать события, а также методы для вывода списка событий на определенную дату и период времени.

 

Задание 2

Разработать класс для представления объектов типа «Учебный курс», включающий в себя информацию: название курса, преподаватель, дата начала, дата окончания. Используя этот класс как базовый, создать производные классы для представления объектов типа «Очный курс» и «Дистанционный курс» (состав полей определить самостоятельно с учетом того, чтобы их было достаточно для описания каждого типа курса). 

              Классы должны включать в себя основной конструктор объектов класса.

              В производных классах – конструктор объектов производного класса, принимающий объекты базового класса.

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

 

Задание 3

Разработать класс для представления даты (с точностью до дня). Класс должен обеспечивать задание даты от 1 января 1900 года до 31 декабря 2099 года. 

Класс должен содержать:

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

              конструктор даты по заданному количеству дней, прошедших с 1 января 1900 года;

              конструктор по умолчанию;

              методы, возвращающие количество лет, месяцев, дней для данной даты;

              метод, возвращающий количество дней с 1 января 1900 года до данной даты;

              перегруженную операцию сложения двух дат;

              аналогично перегрузить +=;

              перегруженную   операцию   вычитания,          определяющую

промежуток времени между двумя данными датами;

              аналогично перегрузить -=;

              перегруженную операцию == для сравнения двух дат;

              перегруженную операцию < для сравнения двух дат.

 

Задание 4

Необходимо разработать класс "Интернет-магазин". 

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

Класс "Интернет-магазин" должен иметь методы для добавления, удаления и редактирования продуктов, а также методы для поиска продуктов по различным критериям, таким как название, цена и количество на складе.

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

 

 

                 

Приложение А

Многофайловые программы

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

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

Ниже будут показаны способы создания и работы с многофайловыми проектами, актуальные в Visual Studio 2022.

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

 

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

 

Перед вам откроется окно создания файла, в котором нужно выбрать «Файл заголовка».

 

После этого вы попадете в заголовочный файл Header.h. В этом файле

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

 

Теперь существует два способа создать определение этой функции:

Первый способ - нажать на значок «Отвертки», после чего появится пункт меню «Создать определение».

 

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

Второй способ – самостоятельно создать исполняемый файл таким же способ, как файл заголовка, только создавать его нужно в папке «Исполняемые файлы» и при создании выбрать файл с расширением .cpp. Но теперь нужно будет подключить к нему заголовочный файл, делается это с помощью директивы include и указания в двойных кавычках названия заголовочного файла:

#include "Hello World.h"

Определяется функция также, как если бы она определялась в однофайловой программе. Важно, что для того, чтобы работать в консоли и использовать команды по типу cout и cin, нужно в заголовочном файле подключить библиотеку iostream. Содержимое файла “Hello World.cpp”:

#include "Hello World.h" using namespace std;

 

void Hello_World()

{

                           cout << "Hello World!" << endl;

}

Чтобы использовать эту функцию в «главном» файле нужно подключить ее заголовочный файл с помощью include.

#include <iostream> #include "Hello World.h" using namespace std;

 

int main() {

                         setlocale(LC_ALL, "ru");

 

                    Hello_World();

 

                   return 0;

}

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

Далее перейдем к классам. Для того, чтобы создать класс автоматически, средствами Visual Studio, нужно правой кнопкой мыши нажать на название проекта в обозревателе решений, после чего «Добавить – Класс» и откроется окно, в котором можно ввести имя класса, и является ли он наследником какого-то другого класса. 

 

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

#pragma once #include <iostream> using namespace std; class Hero { private:

                int level;                string race;  double hp; public:

                      void attack();

                     void walk();

                                 Hero(int level, string race, double hp);

                             Hero(int level, string race);

                       Hero(int level);

                  Hero();

                  ~Hero();

};

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

#include "Hero.h"

 

Hero::Hero(int level) : Hero()

{

                         this->level = level;

}

 

Hero::Hero(int level, string race) : Hero(level)

{

                       this->race = race;

}

 

Hero::Hero(int level, string race, double hp) : Hero(level, race)

{

                     this->hp = hp;

}

 

Hero::Hero()

{

                this->level = 0;      this->hp = 0;

                       this->race = "None";

}

 

Hero::~Hero()

{ }

void Hero::attack()

{

                          cout << "Attack!!" << endl;

void Hero::walk()

{

                          cout << "Walk..." << endl;

}

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

Для использования наследования в разных модулях, допустим создать класс Маг на основе классе Герой, можно создать класс Маг как в одном модуле с классом Герой, так и создать отдельный. В таком случае для наследования нужно подключить в заголовочный файл модуль базового класса. В таком случае подключать библиотеку iostream не нужно, потому что она подключена к модулю базового класса с помощью include, а он, как уже известно, просто копирует весь свой код в место, куда подключен. Для примера создадим модуль класса Маг. Заголовочный файл mage.h:

#pragma once #include "Hero.h" class mage : public Hero

{ private:

                int intelligence; public:        mage(int intelligence, int level, string race, double hp);                void attack();

};

И сам исполняемый файл:

#include "mage.h"

 

mage::mage(int intelligence, int level, string race, double hp) : Hero(level, race, hp) {

                              this->intelligence = intelligence;

void mage::attack()

{

                        cout << "Vzhuhh" << endl;

}

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

Как вы уже могли заметить, в начале каждого заголовочного файла присутствует директива #pragma once, она используется для «защиты заголовков», а именно от их повторного определения. Допустим, нам нужно подключить к модулю Hero модуль mage, а к модулю mage модуль Hero, таким образом, Hero может использовать функции mage и наоборот. 

#include "Hero.h" class mage : public Hero

 

#include "mage.h" class Hero

Поскольку директива include вставляет весь код из своего модуля в место вызова, то #include "Hero.h" вставит на свое место #include "mage.h", а он вставит #include "Hero.h", а он снова вызовет mage.h и так может происходить до бесконечности, то есть они будут постоянно вызывать друг друга. Для таких случаев и используется #pragme one, она работает так, что весь код, который будет находиться после нее и код cpp файла будут подключены в какой-либо модуль только один раз и если в этот модуль опять будет подключать тот же код, то директива сначала проверит, не подключен ли он уже и только потом допустит или отклонит подключение.

 

Оглавление 1. Знакомство с

Оглавление 1. Знакомство с

Виртуальные функции .......

Виртуальные функции .......

Классы Классы, это определенный тип данных, как integer или double, только такой тип данных мы можем создать сами и добавить в него различные функции и…

Классы Классы, это определенный тип данных, как integer или double, только такой тип данных мы можем создать сами и добавить в него различные функции и…

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

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

Полиморфизм Полиморфизм – возможность объекта вести себя по-разному в зависимости от ситуации и реагировать на какое-то одно действие, но какимто специфическим образом именно для этого…

Полиморфизм Полиморфизм – возможность объекта вести себя по-разному в зависимости от ситуации и реагировать на какое-то одно действие, но какимто специфическим образом именно для этого…

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

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

Studio: после того, как мы поставим точку после переменной, компилятор сам предложит выбрать поля, которые мы описали в теле класса:

Studio: после того, как мы поставим точку после переменной, компилятор сам предложит выбрать поля, которые мы описали в теле класса:

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

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

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

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

Область Private */ };

Область Private */ };

Раса - " << race << endl;

Раса - " << race << endl;

Рассмотрим пример: class Hero { public : double getHp() //Метод получения значения { return hp; } void setHp( double my_hp ) //

Рассмотрим пример: class Hero { public : double getHp() //Метод получения значения { return hp; } void setHp( double my_hp ) //

Задание для самопроверки 2.3

Задание для самопроверки 2.3

Посмотрим, как использовать конструкторы:

Посмотрим, как использовать конструкторы:

Hero( string my_race , int my_level , double my_hp ) { race = my_race ; level = my_level ; hp = my_hp ; }

Hero( string my_race , int my_level , double my_hp ) { race = my_race ; level = my_level ; hp = my_hp ; }

Отсутствует" ; level = 0; } private : double hp; string race; int level; };

Отсутствует" ; level = 0; } private : double hp; string race; int level; };

Hero() //деструктор { delete p; } };

Hero() //деструктор { delete p; } };

Объявляется конструктор копирования следующим образом: class

Объявляется конструктор копирования следующим образом: class

Фактически, здесь уже используется ключевое слово this, только неявно, как это делается с конструктором по умолчанию

Фактически, здесь уже используется ключевое слово this, только неявно, как это делается с конструктором по умолчанию

Hero &level_up() { this ->level += 1; this ->hp += 10; return * this ; }

Hero &level_up() { this ->level += 1; this ->hp += 10; return * this ; }

Hero( double hp , string race , int level ) { this ->hp = hp ; this ->race = race ; this ->level = level…

Hero( double hp , string race , int level ) { this ->hp = hp ; this ->race = race ; this ->level = level…

Статические методы класса Статические методы, ровно, как и поля, не имеют привязанности к какому-то объекту класса и могут быть вызваны на уровне имени этого класса

Статические методы класса Статические методы, ровно, как и поля, не имеют привязанности к какому-то объекту класса и могут быть вызваны на уровне имени этого класса

Hero & hero ) { hero

Hero & hero ) { hero

Enemy() { this ->damage = 0; } void attack(

Enemy() { this ->damage = 0; } void attack(

Hero & hero ); В качестве аргумента передается объект класса герой, которому будет нанесен урон

Hero & hero ); В качестве аргумента передается объект класса герой, которому будет нанесен урон

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

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

Hero() { this ->hp = 0; this ->race = "Отсутствует" ; this ->level = 0; }

Hero() { this ->hp = 0; this ->race = "Отсутствует" ; this ->level = 0; }

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

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

Hero & Hero :: operator += ( const

Hero & Hero :: operator += ( const

По умолчанию присваивание осуществляет поэлементное копирование значений полей

По умолчанию присваивание осуществляет поэлементное копирование значений полей

Hero & hero ); В качестве аргумента передается ссылка на объект нашего класса, после чего, сохраняя сигнатуру, перегрузка определяется за классом: ostream & operator<< (…

Hero & hero ); В качестве аргумента передается ссылка на объект нашего класса, после чего, сохраняя сигнатуру, перегрузка определяется за классом: ostream & operator<< (…

Не стоит забывать, что эту перегрузку тоже нужно сделать дружественной

Не стоит забывать, что эту перегрузку тоже нужно сделать дружественной

Также добавьте методы для сортировки и поиска элементов в массиве

Также добавьте методы для сортировки и поиска элементов в массиве

Здоровье, Уровень и Раса, в реальности таких одинаковых полей может быть сколь угодно много и для каждого класса придется описывать эти поля отдельно, помимо этого…

Здоровье, Уровень и Раса, в реальности таких одинаковых полей может быть сколь угодно много и для каждого класса придется описывать эти поля отдельно, помимо этого…

Видим, что объект класса лучник имеет доступ к полям класса герой

Видим, что объект класса лучник имеет доступ к полям класса герой

Задания для самопроверки 3.0

Задания для самопроверки 3.0

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

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

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

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

После двоеточия пишется название класса, а в скобочках аргументы, которые передаются в базовый конструктор, таким образом при создании объекта сначала будут определены поля базового класса,…

После двоеточия пишется название класса, а в скобочках аргументы, которые передаются в базовый конструктор, таким образом при создании объекта сначала будут определены поля базового класса,…

Создадим класс Игрок, который управляет героем, и который будет давать команду атаковать

Создадим класс Игрок, который управляет героем, и который будет давать команду атаковать

Beat" << endl; } }; class

Beat" << endl; } }; class

Player { public : void beat(

Player { public : void beat(

Для таких целей используются абстрактные классы - это классы, объекты которых невозможно создать и они служат базовым классом для его наследников

Для таких целей используются абстрактные классы - это классы, объекты которых невозможно создать и они служат базовым классом для его наследников

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

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

Warrior { public : int strength; void attack() { cout << "Bzzn" << endl; } }; class

Warrior { public : int strength; void attack() { cout << "Bzzn" << endl; } }; class

Задания для самопроверки 3.5

Задания для самопроверки 3.5

Виртуальное наследование Для лучшего понимая этой темы обратимся к примеру из пункта 3

Виртуальное наследование Для лучшего понимая этой темы обратимся к примеру из пункта 3

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

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

Задания для самопроверки 3.7

Задания для самопроверки 3.7

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

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