Введение в объектно-ориентированное программирование

 

Объектно-ориентированное, или объектное, программирование (в дальнейшем ООП) – парадигма программирования, в которой основными концепциями являются понятия объектов и классов.
 

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

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

В языке C++ классы описываются ключевым словом class:

Приведём в качестве примера описание класса Apple, описывающего яблоко:

В данном примере мы имеем объявление собственного типа данных – класса Apple, а также создание объекта GreenApple класса Apple. Класс имеет поле color типа unsigned int, указывающий, какого цвета яблоко, а также метод eat() типа void. Сейчас Apple представляет собой класс, описывающий одну характеристику цвета и функцию "съесть".

При создании объекта вызывается конструктор класса. Конструктор класс – это метод класса, вызываемый при создании объекта и служащий для задания данным объекта начальных значений. Конструктор имеет то же имя, что и класс и не имеет типа. Указывать конструктор не обязательно; если конструктор не объявлен, при создании объекта будет вызван стандартный конструктор, который просто создаст данные-члены объекта в памяти, при этом в них будет находиться «мусор», поэтому желательно всегда явно указывать конструктор, задающий начальные значения полям объекта.

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

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

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

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

Ниже приведён пример обновлённого класса Apple, который включает новый параметр weight – вес яблока в граммах и объявления конструкторов вышеописанных трёх типов:


Первый – пустой конструктор, вызывающийся всегда, когда явно не указан вызов другого конструктора. В нём при помощи списка инициализации  цвет яблока задаётся как белый, а вес – 100 грамм.

Далее идёт конструктор, в который передаются все параметры нового яблока (вес и цвет).

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

 

Обращения к членам класса осуществляется через «.» – оператор выбора члена, а если используется указатель на объект, то посредством «->»оператора непрямого выбора члена, который используется вместо точки.

 

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

Оператор «::» называется оператором расширения видимости, он указывает, из какого класса данный метод, после оператора идёт собственно имя метода. В целом реализация метода вне класса не отличается от реализации обычной функции, только вместе с именем метода необходимо указывать класс, из которого этот метод, при помощи «::».

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

Давайте добавим члены класса и дополним класс Apple до более полного описания яблока:


Здесь описаны новые методы ccal и price, возвращающие калорийность и цену яблока соответственно. Также созданы объекты GreenApple, RedApple и RedApple2, демонстрирующие работу конструкторов. Для GreenApple вызван конструктор без параметров, для RedApple поля проинициализированы значениями белого цвета и веса в 500 грамм, а RedApple2 создан как копия RedApple и после создания имеет те жеполя, что и RedApple. Далее членам GreenApple присваиваются другие значения, зелёный цвет и вес, равный весу RedApple.

 

Объектно-ориентированное программирование включает, помимо классов и объектов, ещё и такие понятия, как: наследование, инкапсуляция и полиморфизм.

 

Инкапсуляция – свойство классов предоставлять программисту только те методы для изменения полей системы, которые предусмотрены для данного класса. Решение задачи может потребовать сокрытие от программиста непосредственного доступа к полям класса. Например, в классе Apple может потребоваться скрыть от программиста возможность непосредственного доступа к весу яблока, чтобы программист не применял к нему операцию, например, синуса. Чтобы предотвратить непредназначенные для класса операции с полями, с полями следует использовать модификатор private:

В новом классе Apple не будет доступа к членам color и weight извне класса, ими смогут пользоваться только методы класса. Но как же тогда узнать вес яблока в программе? Всё просто, для этого лишь нужно объявить метод, возвращающий значение веса яблока, который можно использовать вне класса:

Здесь описана функция getWeight(), возвращающая вес яблока, таким образом, вес яблока теперь нельзя непосредственно изменить. Чтобы изменить вес яблока, можно описать функцию-член void setWeight(), устанавливающую вес яблока, или, если требуется другой способ изменить вес, можно, например, описать метод void grow(), который «заставляет» яблоко расти, то есть увеличивать вес, изменять цвет и т.д.
 

Всего в C++ существует 3 вида членов класса, с точки зрения инкапсуляции: public, protected и private. public позволяет использовать член класса в любом мете программы, private – только изнутри класса, а protected используется вкупе с наследованием, о нём речь пойдёт ниже.
 

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

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

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

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

Наследование позволяет классифицировать классы, как, например, классифицируют животных и растения. Яблоко и груша – это фрукты, они имеют много общего, например, у них можно вычислить калорийность. В программе было быэкономичнее написать одну общую функцию int ccal(), возвращающую калорийность фрукта, без привязки к конкретному фрукту. Также оба фрукта имеют вес и цвет, что тоже можно описать один раз, экономя время на написание кода. В приведённом примере про фрукты класс фрукта будет именоваться базовым классом, причём он будет даже прямым базовым классом. Логично, что классы могут наследоваться не напрямую, класс фрукт может быть унаследован у класса растение, тогда класс растение будет базовым классом для классов яблоко и груша, однако он будет уже непрямым базовым классом. Базовый класс – это класс, от которого наследуется данный класс, если наследование напрямую – это прямой базовый класс, если через другие классы – непрямой базовый класс.
 

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

 

Наследование в C++ реализуется следующим образом:

Здесь Plant, Fruit, Apple и Pear – классы растение, фрукт, яблоко и груша соответственно. Механизм наследования реализуется таким образом, что объявляется сначала базовый класс, затем наследуемый, как обычный класс, только сразу после имени класса через двоеточие указывается имя базового класса.
 

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

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

Наследование в C++ тоже бывает трёх видов: public, protected и private. Вид наследования указывается перед именем базового класса при описании потомка:

Здесь Fruit наследуется посредством public-наследования, а Apple и Pear посредством private-наследования.
 

При public-наследовании члены, указанные как protected и public в потомке остаются protected и public соответственно; при protected-наследовании protected и public члены базового класса станут protected; при private-наследовании public и protected члены базового класса станут в потомке private.
 

Как и с инкапсуляцией, по умолчанию используется private-наследование.
 

Далее приведён пример, поясняющий вышесказанное:

Ниже мы видим пример использования общего члена для классов Apple и Pear:

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

На самом деле, есть средство ООП, позволяющее осуществить вышесказанное, это третий кит, на котором стоит ООП – полиморфизм.
 

Полиморфизм — это свойство системы использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта. Иными словами, мы можем воспользоваться функциями grow() и ccal() для экземпляров классов Apple и Pear и получить результат, исходя из особенностей этих классов. При этом, вдаваться в тип конкретного фрукта не необходимости, нам достаточно знать, что это фрукт, тогда мы можем воспользоваться этими функциями.
 

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

Но снова, вызвав функции ccal() и grow() для груши и яблока, мы получим, что они работают также как и в классах Fruit и Plant. Всё правильно, ведь классы Fruit и Plant не предполагают того, чтобы эти функции были изменены в потомках классов.

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

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

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

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

Важно, что virtual-функцией не может быть конструктор.
 

Если есть надобность в написании конструктора для унаследованного класса, надо знать ещё одну тонкость: необходим вызов конструктора прямого базового класса. Реализуется это несложно:

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

Аналогично конструктору, в классе можно описать ещё одну специфическую функцию – деструктор.
 

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

Прототип у деструктора почти такой жекак и у конструктора, он именуется именем класса и предваряется «~». Деструктор вызывается при вызове оператора delete или при любом другом удалении объекта.
 

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

Пользу ООП можно продемонстрировать на следующем примере.
 

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

Таким образом, мы имеем массив указателей  из тысячи элементов на тип Fruit, далее для каждого из них выделяем память под тот фрукт, который нужен: яблоко или грушу. Это позволяет теперь пройти циклом по всему массиву и у каждого элемента вызвать метод grow(), чтобы каждый фрукт вырос, но мы не вдаёмся в подробности отдельного объекта – объект сам решает для себя, что он будет делать при вызове функции. На плечах программиста, таким образом, лежит совсем другая задача управления этими объектами.
                                                                                                                                                                                                                                                                                                                                           Прокошев Inc.

Адрес: 614039, г. Пермь, ул. Комсомольский проспект, 45
Телефон: +7 (342) 212-80-71
E-Mail: school9-perm@ya.ru
Вопрос администратору сайта