Классы в программировании пример
Урок №113. Классы, Объекты и Методы
Обновл. 2 Фев 2020 |
Хотя C++ предоставляет ряд фундаментальных типов данных (например: char, int, long, float, double и т.д.), которых бывает достаточно для решения относительно простых проблем, для решения сложных проблем функционала этих простых типов может не хватать.
Классы
Одной из наиболее полезных фич языка C++ является возможность определять собственные типы данных, которые будут лучше соответствовать в решении конкретных проблем. Вы уже видели, как перечисления и структуры могут использоваться для создания собственных пользовательских типов данных. Например, структура для хранения даты:
Перечисления и структуры — это традиционный (не объектно-ориентированный) мир программирования, в котором мы можем только хранить данные. В C++11 мы можем создать и инициализировать структуру следующим образом:
Для вывода даты на экран (что может понадобиться выполнить и не раз, и не два) хорошей идеей будет написать отдельную функцию. Например:
В объектно-ориентированном программировании типы данных могут не только содержать данные, но и функции, которые будут работать с этими данными. Для определения такого типа данных в C++ используется ключевое слово class. Использование ключевого слова class определяет новый пользовательский тип данных — класс.
В C++ классы очень похожи на структуры, за исключением того, что они обеспечивают гораздо большую мощность и гибкость. Фактически, следующая структура и класс по функционалу идентичны:
Единственным существенным отличием здесь является public — ключевое слово в классе. О нём мы поговорим детальнее в следующем уроке.
Так же, как объявление структуры, так же и объявление класса не приводит к выделению какой-либо памяти. Для использования класса нужно объявить переменную этого типа класса:
В C++ переменная класса называется экземпляром или объектом класса. Точно так же, как определение переменной фундаментального типа данных (например, int x ) приводит к выделению памяти для этой переменной, так же и создание объекта класса (например, DateClass today ) приводит к выделению памяти для этого объекта.
Методы классов
Помимо хранения данных, классы также могут содержать и функции! Функции, определённые внутри класса, называются функциями-членами или методами. Методы могут быть определены внутри или вне класса. Пока что мы будем определять их внутри класса (для простоты), как определить их вне класса — рассмотрим несколько позже.
Класс Date с методом вывода даты:
Обратите внимание, как эта программа похожа на программу выше (где используется структура).
Однако есть несколько отличий. В версии DateStruct для print() нам нужно было передать переменную структуры непосредственно в функцию print() в качестве параметра. Если бы мы этого не сделали, то print() не знал бы, какую переменную DateStruct выводить. Нам тогда бы пришлось явно ссылаться на члены структуры внутри функции.
Методы класса работают несколько иначе: все вызовы функций-членов должны быть связаны с объектом класса. Когда мы вызываем today.print() , мы сообщаем компилятору вызвать метод print() объекта today .
Рассмотрим определение метода print() ещё раз:
Поэтому, при вызове today.print() , компилятор интерпретирует:
m_day как today.m_day ;
m_month как today.m_month ;
m_year как today.m_year .
Если бы мы вызвали tomorrow.print() , то m_day ссылался бы на tomorrow.m_day .
По сути, связанный объект неявно передаётся функции-члену. По этой причине его часто называют неявным объектом.
Детальнее о том, как передаётся неявный объект функции-члену, мы поговорим в следующих уроках. Ключевым моментом здесь является то, что для работы с функциями, не являющимися членами класса, нам нужно передавать данные в эту функцию явно (в качестве параметров). А для работы с методами у нас всегда есть неявный объект класса!
Использование префикса m_ (англ. «m» = «members») для переменных-членов помогает различать переменные-члены от параметров функции или локальных переменных внутри методов класса. Это полезно по нескольким причинам:
Во-первых, когда мы видим переменную с префиксом m_ , то мы понимаем, что работаем с переменной-членом класса.
Во-вторых, в отличие от параметров функции или локальных переменных, объявленных внутри функции, переменные-члены объявляются в определении класса. Следовательно, если мы хотим знать, как объявлена переменная с префиксом m_ , то мы понимаем, что искать нужно в определении класса, а не внутри функции.
Обычно программисты пишут имена классов с заглавной буквы.
Правило: Пишите имена классов с заглавной буквы.
Вот ещё один пример программы с использованием класса:
Name: Max
Id: 6
Wage: $32.75
В отличие от обычных функций, порядок, в котором определены методы класса, не имеет значения!
Примечание о структурах в C++
В языке C структуры могут только хранить данные и не могут иметь связанных методов. В C++, после проектирования классов (используя ключевое слово class), Бьёрн Страуструп размышлял о том, нужно ли, чтобы структуры (которые были унаследованы из языка С) имели связанные функции-члены. После некоторых размышлений он решил, что нужно. Поэтому в программах выше мы также можем использовать ключевое слово struct, вместо class, и всё будет работать!
Многие разработчики (включая и меня) считают, что это было неправильное решение, поскольку оно может привести к проблемам: например, справедливо предположить, что класс выполняет очистку памяти после себя (например, класс, которому выделена память, освободит её до того, как будет уничтожен), но предполагать то же самое при работе со структурами — небезопасно. Следовательно, рекомендуется использовать ключевое слово struct для структур, используемых только для хранения данных и ключевое слово class для определения объектов, которые требуют объединения как данных, так и функций.
Правило: Используйте ключевое слово struct для структур, используемых только для хранения данных. Используйте ключевое слово class для объектов, объединяющих как данные, так и функции.
Заключение
Оказывается, стандартная библиотека C++ полна классов, созданных для нашего удобства. std::string, std::vector и std::array — это всё типы классов! Поэтому, когда вы создаёте объект любого из этих типов, вы создаёте объект класса. А когда вы вызываете функцию с использованием этих объектов, вы вызываете метод:
BestProg
C++. Классы. Часть 1. Понятие класса. Объявление класса. Объект класса. Классы в среде CLR . Инкапсуляция данных в классе
Содержание
1. Основные понятия объектно-ориентированного программирования. Классы и объекты
В языке программирования C++ понятие «класс» лежит в основе объектно-ориентированного программирования (ООП). Объектно-ориентированное программирование возникло как усовершенствование процедурно-ориентированного программирования.
В свое время, процедурно-ориентированное программирование уже не обеспечивало необходимого качества написания больших программных систем. Программы разбивались (структурировались) на модули, возникала повторяемость программного кода в разных модулях (частях) программы, усложнялось тестирование (поиск ошибок), снижалась надежность программы.
В основе ООП лежат понятия «объект» и «класс». В языке программирования объект – это переменная типа «класс». Класс описывает данные и методы (функции), которые будут использоваться объектом этого класса. Каждый класс описывает логически-завершенную единицу программы. Инкапсуляция данных и методов их обработки в пределах класса позволяет улучшить структурированность программных систем. Это в свою очередь уменьшает риск возникновения «невидимых» логических ошибок. Использование наследственности и полиморфизма в классах позволяет избежать повторяемости программного кода и удобно упорядочить сложные вызовы методов, объединенных между собой в список.
Класс определяет формат (описание) некоторых данных и работу (поведение) над этими данными. Из объявления класса можно получить различное количество объектов класса (переменных типа «класс»). Каждый объект класса определяется конкретным (на данный момент) значением внутренних данных (переменных), которое называется состоянием объекта.
В классе объявляются данные (внутренние переменные, свойства) и методы (функции), которые оперируют этими данными (выполняют работу над данными).
Класс может быть унаследован от других классов верхних уровней. Это означает, что класс может использовать часть кода других классов верхних уровней.
Также класс может быть родительским для других классов, которые наследуют его программный код.
2. Какие виды классов языка C++ можно реализовать в среде CLR ?
В среде CLR ( Common Language Runtime ) поддерживаются два вида классов:
- неуправляемые ( unmanaged ) классы. Для выделения памяти под объекты таких классов могут использоваться неуправляемые указатели ( * ) и операция new ;
- управляемые ( managed ) классы. Для выделения памяти в таких классах могут быть использованы управляемые указатели ( ^ ) и операция gcnew .
Данная тема освещает особенности использования unmanaged ( * ) классов.
Примеры, которые демонстрируют особенности использования и отличие между управляемыми ( ^ ) и неуправляемыми ( * ) классами более подробно описываются в теме:
3. Общая форма объявления unmanaged -класса. Ключевое слово «class»
В простейшем случае (без наследования) общая форма объявления unmanaged -класса имеет следующий вид
- имя_класса – непосредственно имя нового типа данных «класс». Это имя используется при создании объектов класса.
Ключевое слово class сообщает о том, что объявляется новый класс (тип данных). Внутри класса объявляются члены класса: данные и методы. Ключевое слово private определяет члены класса, которые должны быть закрыты от внешних методов, объявленных за пределами класса, а также объектов класса. Члены данных, объявленные с ключевым словом private , доступны только другим членам этого класса.
Ключевое слово public определяет общедоступные данные (переменные) и методы (функции) класса.
Ключевое слово protected определяет защищенные данные и методы класса, которые есть:
- доступными для методов унаследованных от данного класса;
- недоступными для методов, реализованных в других частях программы;
- недоступными для объектов (экземпляров) класса.
В пределах описания класса секции (разделы) private , protected , public могут следовать в любом порядке и в любом количестве. Например:
4. Что означает термин «инкапсуляция данных» в классе?
Термин «инкапсуляция данных» означает то, что для членов класса (данных и методов) можно устанавливать степень доступности из других частей программного кода (других методов, объектов класса). Таким образом, возникает понятие скрытия данных (методов) в классе.
Инкапсуляция обеспечивает улучшение надежности сохранения данных в классе путем ввода дополнительных методов проверки этих данных на допустимые значения. Как правило, доступ к скрытым данным в классе происходит не напрямую, а через вызовы специальных методов доступа или свойств класса. Непосредственно данные размещаются в скрытой секции (разделе) класса, а методы доступа к этим данным размещаются в общедоступном разделе класса.
Классический язык C++ позволяет устанавливать доступ к членам класса с помощью трех спецификаторов: private , protected , public .
5. Какие типы доступа могут иметь члены класса? Какие различия между членами класса, объявленными с ключевыми словами private , protected , public ?
Члены класса могут иметь три основных типа доступа, которые определяются соответствующими ключевыми словами:
- private – члены класса есть скрытыми. Это означает, что доступ к ним имеют только методы, которые объявлены в классе. private-члены класса есть недоступными из унаследованных классов и объектов этого класса;
- protected – члены класса есть защищенными. Это означает, что доступ к protected-членам имеют методы данного класса и методы унаследованных классов. protected-члены класса есть недоступными для объектов этого класса;
- public – члены класса есть открытыми (доступными) для всех методов и объектов из всех других частей программного кода.
6. Может ли класс, при его объявлении, содержать только данные и не содержать методов?
Класс может быть объявлен без методов. Такие классы содержат только данные. Чтобы получить доступ к данным в классе, не содержащим методов, нужно эти данные объявить в разделе public . Классы без методов почти не применяются. Если объявить данные в разделе private , то получить доступ к членам-данным класса будет невозможно.
Пример. В данном примере объявляется класс без методов, который реализует операции над датой. Класс содержит внутренние переменные (данные), что представляют собой:
Фрагмент кода, который демонстрирует работу с классом CMyDate
7. Пример объявления пустого класса
Да, класс может быть объявлен без данных и без методов. Например, ниже объявлен класс, который не содержит ни данных ни методов.
Объект такого класса также создается.
Понятно, что такой класс имеет сильно ограниченное использование. Пустой класс целесообразно создавать в случаях, если во время создания большого проекта нужно протестовать его более раннюю (начальную) версию, в который некоторые классы еще не разработаны и не реализованы. Вместо реального класса указывается пустой класс – заглушка. Этот пустой класс разрабатывается под потребности будущего проекта таким образом, чтобы компилятор не выдавал сообщения об ошибке и можно было протестовать ту часть кода, которая на данный момент уже написана. В этом случае имя пустого класса выбирается таким, каким оно должно быть в будущем проекте.
8. Пример класса, содержащего методы (функции)
Основные преимущества классов обнаруживаются при наличии методов – членов класса. С помощью методов доступа к данным в классах можно удобно:
- инициализировать данные начальными значениями;
- осуществлять проверку на корректность внутренних данных при их задании;
- реализовывать различные варианты преобразования (конвертирования) данных;
- реализовывать организацию выделения памяти для сложных типов данных, которые реализованы в классах;
- выполнять разнообразные виды вычислений над данными.
Пример. Модификация класса CMyDate . Класс, который описывает дату и операции над ней. Операции доступа к членам класса реализованы с помощью соответствующих методов. Сами данные реализованы в разделе private .
Программный код класса
Реализация методов класса SetDate() , GetDay() , GetMonth() , GetYear()
Использование методов класса из другого программного кода (например, обработчика события в приложениях типа Windows Forms)
9. В каких частях класса и программы можно объявлять реализацию методов класса? Пример
Реализацию методов класса можно объявлять в классе и за пределами класса.
Например. В приведенном ниже программном коде объявляется класс СMyTіме . Класс содержит два метода SetTime1() и SetTime2() , которые выполняют одинаковую работу: устанавливают новое время. Тело (реализация) метода SetTime1() описывается в классе CMyTime . Реализация метода SetTime2() описывается за пределами класса. В классе описывается только прототип (декларация) метода SetTime2() .
Тело метода, который описывается за пределами класса, может быть описано в другом модулн. Как правило, в системе Microsoft Visual Studio этот модуль имеет расширение *.cpp . Сам же класс описывается в модуле с расширением *.h .
10. Какое назначение имеет оператор расширения области видимости (доступа) ‘::’ ?
Программный код методов-членов класса можно описывать в самом классе и за его пределами. Если нужно описать код метода, который есть членом класса, то для этого используется оператор расширения области видимости «::» . Оператор «::» определяет имя члена класса вместе с именем класса, в котором он реализован.
11. Что такое объект класса? Какие отличия между объектом класса и объявлением класса? Объявление объекта класса
Объявление класса – это описание формата типа данных. Этот тип данных «класс» описывает данные и методы, которые оперируют этими данными. Описание класса – это только описание (объявление). В этом случае память для класса не выделяется.
Память выделяется только тогда, когда класс используется для создания объекта. Этот процесс еще называют созданием экземпляра класса, который представляет собой физическую сущность класса.
Объявление объекта класса (экземпляра) ничем не отличается от объявления переменной:
С помощью имени имя_объекта можно осуществить доступ к общедоступным ( public ) членам класса. Это осуществляется с помощью символа ‘ . ‘ (точка).
Возможен также вариант объявления указателя на класс. Если это unmanaged -класс, то объявление имеет вид:
После такого объявления, нужно выделять память для объекта класса с помощью оператора new . Доступ к данным по указателю осуществляется с помощью комбинации символов ‘->’ точно так же как и в случае со структурами.
Например. Объявление класса Worker , описывающего методы и данные о работнике предприятия.
Объект класса – это переменная типа «класс». При объявлении объекта класса выделяется память для этого объекта (переменной). Например, для класса Worker можно написать следующий код
Из объекта можно иметь доступ только к public -членам класса. Это можно осуществлять с помощью символа ‘ . ‘ (точка) или доступа по указателю ‘->’ :
12. Какой тип доступа по умолчанию имеют члены класса в C++?
По умолчанию, члены класса имеют доступ private . Поэтому, при объявлении класса, если нужно указать private -члены класса, это слово можно опустить.
Например, пусть заданы объявления класса, который описывает пиксель на экране монитора.
По всей видимости, в начале объявления класса, раздел private отсутствует. Это означает, что члены-данные класса color , x , y есть скрытыми. При создании объекта класса и прямом доступе к ним компилятор выдаст ошибку
13. Каким образом можно реализовать доступ к private -членам класса?
Как правило, private -члены класса есть закрытыми. Это есть основное преимущество инкапсуляции. Чтобы изменять значения private -членов класса, используют методы класса, которые объявлены в public -секции. В этих методах можно изменять значения private -членов. Такой подход используется для обеспечения надежности сохранения данных в private-членах. В public-методах, которые имеют доступ к private -членам, можно реализовать дополнительные проверки на допустимость значений.
Например.
Пусть дан класс, который определяет массив из n вещественных чисел. Класс содержит два скрытых ( private ) члена данных:
- n — количество элементов массива;
- A — непосредственно массив.
Судя из логики задачи, в таком классе количество элементов массива не может быть меньше нуля (
Классы в C++ — урок 10
Весь реальный мир состоит из объектов. Города состоят из районов, в каждом районе есть свои названия улиц, на каждой улице находятся жилые дома, которые также состоят из объектов.
Практически любой материальный предмет можно представить в виде совокупности объектов, из которых он состоит. Допустим, что нам нужно написать программу для учета успеваемости студентов. Можно представить группу студентов, как класс языка C++. Назовем его Students .
Основные понятия
Классы в программировании состоят из свойств и методов. Свойства — это любые данные, которыми можно характеризовать объект класса. В нашем случае, объектом класса является студент, а его свойствами — имя, фамилия, оценки и средний балл.
У каждого студента есть имя — name и фамилия last_name . Также, у него есть промежуточные оценки за весь семестр. Эти оценки мы будем записывать в целочисленный массив из пяти элементов. После того, как все пять оценок будут проставлены, определим средний балл успеваемости студента за весь семестр — свойство average_ball .
Методы — это функции, которые могут выполнять какие-либо действия над данными (свойствами) класса. Добавим в наш класс функцию calculate_average_ball() , которая будет определять средний балл успеваемости ученика.
- Методы класса — это его функции.
- Свойства класса — его переменные.
Функция calculate_average_ball() просто делит сумму всех промежуточных оценок на их количество.
Модификаторы доступа public и private
Все свойства и методы классов имеют права доступа. По умолчанию, все содержимое класса является доступным для чтения и записи только для него самого. Для того, чтобы разрешить доступ к данным класса извне, используют модификатор доступа public . Все функции и переменные, которые находятся после модификатора public , становятся доступными из всех частей программы.
Закрытые данные класса размещаются после модификатора доступа private . Если отсутствует модификатор public , то все функции и переменные, по умолчанию являются закрытыми (как в первом примере).
Обычно, приватными делают все свойства класса, а публичными — его методы. Все действия с закрытыми свойствами класса реализуются через его методы. Рассмотрим следующий код.
Мы не можем напрямую обращаться к закрытым данными класса. Работать с этими данными можно только посредством методов этого класса. В примере выше, мы используем функцию get_average_ball() для получения средней оценки студента, и set_average_ball() для выставления этой оценки.
Функция set_average_ball() принимает средний балл в качестве параметра и присваивает его значение закрытой переменной average_ball . Функция get_average_ball() просто возвращает значение этой переменной.
Программа учета успеваемости студентов
Создадим программу, которая будет заниматься учетом успеваемости студентов в группе. Создайте заголовочный файл students.h, в котором будет находиться класс Students .
Мы добавили в наш класс новые методы, а также сделали приватными все его свойства. Функция set_name() сохраняет имя студента в переменной name , а get_name() возвращает значение этой переменной. Принцип работы функций set_last_name() и get_last_name() аналогичен.
Функция set_scores() принимает массив с промежуточными оценками и сохраняет их в приватную переменную int scores[5] .
Теперь создайте файл main.cpp со следующим содержимым.
В самом начале программы создается объект класса Students . Дело в том, что сам класс является только описанием его объекта. Класс Students является описанием любого из студентов, у которого есть имя, фамилия и возможность получения оценок.
Объект класса Students характеризует конкретного студента. Если мы захотим выставить оценки всем ученикам в группе, то будем создавать новый объект для каждого из них. Использование классов очень хорошо подходит для описания объектов реального мира.
После создания объекта student , мы вводим с клавиатуры фамилию, имя и промежуточные оценки для конкретного ученика. Пускай это будет Вася Пупкин, у которого есть пять оценок за семестр — две тройки, две четверки и одна пятерка.
Введенные данные мы передаем set-функциям, которые присваивают их закрытым переменным класса. После того, как были введены промежуточные оценки, мы высчитываем средний балл на основе этих оценок, а затем сохраняем это значение в закрытом свойстве average_ball , с помощью функции set_average_ball() .
Скомпилируйте и запустите программу.
Отделение данных от логики
Вынесем реализацию всех методов класса в отдельный файл students.cpp.
А в заголовочном файле students.h оставим только прототипы этих методов.
Такой подход называется абстракцией данных — одного из фундаментальных принципов объектно-ориентированного программирования. К примеру, если кто-то другой захочет использовать наш класс в своем коде, ему не обязательно знать, как именно высчитывается средний балл. Он просто будет использовать функцию calculate_average_ball() из второго примера, не вникая в алгоритм ее работы.
Над крупными проектами обычно работает несколько программистов. Каждый из них занимается написанием определенной части продукта. В таких масштабах кода, одному человеку практически нереально запомнить, как работает каждая из внутренних функций проекта. В нашей программе, мы используем оператор потокового вывода cout , не задумываясь о том, как он реализован на низком уровне. Кроме того, отделение данных от логики является хорошим тоном программирования.
В начале обучения мы говорили о пространствах имен (namespaces). Каждый класс в C++ использует свое пространство имен. Это сделано для того, чтобы избежать конфликтов при именовании переменных и функций. В файле students.cpp мы используем оператор принадлежности :: перед именем каждой функции. Это делается для того, чтобы указать компилятору, что эти функции принадлежат классу Students .
Создание объекта через указатель
При создании объекта, лучше не копировать память для него, а выделять ее в в куче с помощью указателя. И освобождать ее после того, как мы закончили работу с объектом. Реализуем это в нашей программе, немного изменив содержимое файла main.cpp.
При создании статического объекта, для доступа к его методам и свойствам, используют операция прямого обращения — « . » (символ точки). Если же память для объекта выделяется посредством указателя, то для доступа к его методам и свойствам используется оператор косвенного обращения — « -> ».
Конструктор и деструктор класса
Конструктор класса — это специальная функция, которая автоматически вызывается сразу после создания объекта этого класса. Он не имеет типа возвращаемого значения и должен называться также, как класс, в котором он находится. По умолчанию, заполним двойками массив с промежуточными оценками студента.
Мы можем исправить двойки, если ученик будет хорошо себя вести, и вовремя сдавать домашние задания. А на «нет» и суда нет 🙂
Деструктор класса вызывается при уничтожении объекта. Имя деструктора аналогично имени конструктора, только в начале ставится знак тильды
Что такое классы в объектно-ориентированном программировании
В этом цикле статей мы говорим об объектно-ориентированном программировании — передовом и очень распространённом подходе к разработке. Это стоит знать всем, кто серьёзно относится к программированию и хочет зарабатывать в этой области.
Если не читали предыдущую статью, вот краткое содержание:
- ООП — это подход к программированию. Такой набор практик и принципов, которыми пользуются хорошие разработчики. Противопоставление этому подходу — традиционное процедурное программирование.
- В процедурном программировании мы пишем функции, которые выполняют какие-то задачи. И при необходимости вызываем одни функции из других. В программе функции живут отдельно, данные — отдельно.
- Главная проблема процедурного программирования — сложно писать и поддерживать большие проекты. Любой мало-мальски сложный продукт будет требовать сотен функций, которые будут связаны между собой. Получится «спагетти-код».
- В ООП функции и данные группируются в объекты. Объекты более-менее независимые и общаются друг с другом по строго определённым правилам.
- Данные в ООП хранятся внутри объектов и называются свойствами объектов. Например, у объекта user может быть свойство name со значением ‘Иван’.
- Функции в ООП тоже хранятся внутри объектов и называются методами объектов. Например, у объекта user может быть метод sendEmail(), который отправляет этому юзеру письмо.
- Можно представить, что в ООП взяли «спагетти-код» с тефтелями и разложили из огромного чана порционно по контейнерам. Теперь в каждом контейнере есть спагетти и тефтели, и каждый программист может работать над своим контейнером-объектом, а не ковыряться в общем чане со спагетти.
Одно из преимуществ ООП — не нужно много раз писать один и тот же код. Можно однажды придумать какую-то красивую штуку и потом заново её использовать буквально одной строкой. Для этого и нужны классы.
Что за классы
Вот одно из формальных определений класса: «Класс — это элемент ПО, описывающий абстрактный тип данных и его частичную или полную реализацию»
Если более по-русски, то класс — это шаблон кода, по которому создаётся какой-то объект. Это как рецепт приготовления блюда или инструкция по сборке мебели: сам по себе класс ничего не делает, но с его помощью можно создать новый объект и уже его использовать в работе.
Если пока непонятно, погружайтесь в пример:
Призовём на помощь силу примеров и поговорим про сотовые телефоны.
Допустим, вы делаете мобильники и хотите выпустить на рынок новую модель. Чтобы люди могли сразу пользоваться вашим устройством и быстро к нему привыкли, у телефона должен быть экран, кнопки включения и громкости, камеры спереди и сзади, разъём для зарядки и слот для сим-карты.
Но одного железа недостаточно — нужно соединить его между собой так, чтобы всё работало без сбоёв. Кроме этого, нужно предусмотреть, что происходит при нажатии на кнопки, что выводится на экран и как пользователь будет управлять этим телефоном.
Следующий этап — описать каждую деталь, из которой состоит телефон, каждую микросхему и плату, и объяснить, как детали работают друг с другом. Последний шаг — написать руководство пользователя, где будет полностью рассказано, что можно делать с телефоном, как запустить звонилку или отправить смс.
Мы только что сделали новый класс для телефона — полный набор нужных знаний, описаний, свойств и инструкций, который описывает нашу модель. Все эти инструкции и описания — это ещё не телефон, но из них этот телефон можно сделать.
В программировании у класса есть наборы данных — в нашем случае это комплектующие для телефона. Ещё есть функции для работы с классами, которые называются методами — это то, как пользователь будет работать с нашим телефоном, что он будет на нём делать и каким образом.
Классы на практике
Все примеры дальше мы будем делать на Python, потому что это стильно, модно и молодёжно. А сам Python — очень объектно-ориентированный язык, почти всё в нём — это объекты. Вот и опробуем.
Допустим, мы пишем интернет-магазин с системой скидок. Нам нужно работать с пользователями — постоянными покупателями. Пользователь у нас будет объектом: у него будет имя, возраст и адрес доставки по умолчанию. Мы заведём класс, который поможет нам инициировать нового покупателя.
Здесь сказано: «Вот класс для покупателя. У него есть три свойства: имя, возраст и адрес». Теперь мы можем заводить новых покупателей одной строкой:
# Создаём первого покупателя
# Создаём второго покупателя
Что дальше
В следующем материале мы смоделируем реальную ситуацию: добавим программу лояльности, бонусные баллы и расскажем, как Python с этим справится. Чтобы было интереснее, будем писать код на двух языках сразу — Python и JavaScript.
Классы и объекты
В данном уроке мы рассмотрим классы в C++ и познакомимся с объектно-ориентированным программированием. Объектно-ориентированное программирование или ООП — это одна из парадигм программирования. Парадигма — это, другими словами, стиль. Парадигма определяет какие средства используются при написании программы. В ООП используются классы и объекты. Все наши предыдущие программы имели элементы разных парадигм: императивной, процедурной, структурной.
Мы можем написать одинаковую программу в разных парадигмах. Парадигмы не имеют чёткого определения и часто пересекаются.
Давайте посмотрим на пример. Допустим, в нашей игре есть танки и они могут стрелять, при стрельбе у них уменьшается боезапас. Как мы можем это смоделировать без ООП:
У нас есть структура, которая содержит поле, представляющее количество снарядов, и есть функция атаки, в которую мы передаём танк. Внутри функции мы меняем количество снарядов. Так может выглядеть игра на языке C: структуры отдельно от функций, которые совершают действия со структурными переменными. Данную ситуацию можно смоделировать по-другому с помощью объектно-ориентированного программирования (Object-Oriented Programming, OOP) — ООП.В ООП действия привязываются к объектам.
Определение классов в C++
Класс — это пользовательский тип данных (также как и структуры). Т.е. тип данных, который вы создаёте сами. Для этого вы пишете определение класса. Определение класса состоит из заголовка и тела. В заголовке ставится ключевое слов class, затем имя класса (стандартный идентификатор C++). Тело помещается в фигурные скобки. В C++ классы и структуры почти идентичны. В языке C в структурах можно хранить только данные, но в C++ в них можно добавить действия.
В C++ ключевые слова struct и class очень близки и могут использоваться взаимозаменяемо. У них есть только одно отличие (об этом ниже). Вот как можно определить такой же класс с помощью struct:
Отличие только первом ключевом слове. В одном из прошлых уроков мы уже обсуждали структуры. что мы видим новое? Ключевые слова private и public — это спецификаторы доступа. Также мы видим, что внутри класса мы можем вставлять определения функций.
Определение класса это чертёж. Оно говорит нам из каких данных состоит класс и какие действия он может совершать. т.е. происходит объединение данных и действий в одной сущности.
Переменные и методы класса
Класс состоит из членов класса (class members). Члены класса могут быть переменными (data members) или методами (function members или methods). Переменные класса могут иметь любой тип данных (включая другие структуры и классы). Методы — это действия, которые может выполнять класс. По сути, это обычные функции.
Все методы класса имеют доступ к переменным класса. Обратите внимание, как мы обращаемся к ammo в методе Attack.
Создание объектов класса
Теперь у нас есть свой тип данных и мы можем создавать переменные данного типа. Если после определения структур мы могли создавать структурные переменные, то в случае классов, мы создаём объекты классов (или экземпляры). Разница между классами и структурами только в терминах. Для C++ это почти одно и то же.
Вот так мы можем создать объекты класса Tank и вызвать метод Attack:
t1 и t2 — объекты класса Tank. Для C++ объект класса — это всего-лишь переменная. Тип данных этих переменных — Tank. Ещё раз повторю, что классы (и структуры) позволяют создавать пользовательские типы данных.
В англоязычной литературе создание объектов классов также называется созданием экземпляров — instantiating.
Мы обращаемся к переменным класса и методам с помощью оператора точки (прямой доступ), также как мы обращались к полям структурных переменных.
В нашем примере каждый объект имеет доступ к своей копии ammo. ammo — переменная класса (data member). Attack — метод класса. У каждого объекта своя копия переменных класса, но все объекты одного класса вызывают одни и те же методы.
Размер объекта включает все данные, но не методы
В памяти переменные класса располагаются последовательно. Благодаря этому мы можем создавать массивы объектов и копировать их все вместе (если в классе этих объектов нет динамического выделения памяти). Это будет важно для нас, когда мы начнём работать с графикой в DirectX/OpenGL. Размер объекта класса можно узнать с помощью функции sizeof. При этом в качестве аргумента можно использовать как объект, так и сам класс:
Методы — это все лишь функции. Но в отличии от простых функций, у всех методов есть один скрытый параметр — указатель на объект, который вызывает данный метод. Именно благодаря этому указателю метод знает, какой объект вызвал его и какому объекту принадлежат переменные класса. Внутри метода имя этого указателя — this.
Указатель this
Вот как для компилятора выглядит любой метод:
Это просто иллюстрация. В реальности не нужно указывать аргумент (всё что в круглых скобках). Мы автоматически получаем доступ к указателю this. В данном случае его использование перед ammo необязательно, компилятор автоматически привяжет эту переменную к this.
Указатель this нужен, когда методу необходимо вернуть указатель на текущий объект.
Указатели на объекты
При работе с объектам в C++ вам неизбежно придётся работать с указателями (и ссылками). Как мы помним, при передаче в функцию по значению создаётся копия переменной. Если у вас сложный класс, содержащий большой массив или указатели, то копирование такого объекта может потребовать ненужное выделение дополнительной памяти или может быть вообще невозможным, в случае если в классе вы динамически выделяете память. Поэтому очень часто объекты создаются динамически. Для доступа к таким объектам используется оператор непрямого доступа (стрелочка):
При использовании ссылки на объект, для доступа к его членам используется оператор прямого доступа (точка), т.е. с ссылкой можно обращаться как с обычным объектом:
Чуть ниже мы увидим один случай, когда не обойтись без ссылок.
Конструктор класса (Constructor)
Конструктор класса — метод, вызываемый автоматически при создании объекта. Он используется для инициализации переменных класса и выделении памяти, если это нужно. По сути это обычный метод. Имя обязательно должно совпадать с именем класса и он не имеет возвращаемого значения. Рассмотрим новый класс:
Здесь, в конструкторе задаются начальные значения переменных, но мы можем делать в нём всё что угодно, это обычная функция.
Перегрузка конструктора класса
Перегрузка (overloading) конструктора позволяет создать несколько конструкторов для одного класса с разными параметрами. Всё то же самое, что и при перегрузке функций:
Начальные значения можно задавать в виде списка инициализации. Выше в конструкторе мы инициализировали переменные внутри тела. Список инициализации идёт перед телом конструктора и выглядит так:
В списке инициализации можно задать значение только части переменных класса.
Копирующий конструктор (Copy Constructor)
Без каких-либо действий с нашей стороны мы можем присваивать объектам другие объекты:
Здесь используется копирующий конструктор. Копирующий конструктор по умолчанию просто копирует все переменные класса в другой объект. Если в классе используется динамическое выделение памяти, то копирующий конструктор по умолчанию не сможет правильно создать новый объект. В таком случае вы можете перегрузить копирующий конструктор:
В копирующем конструкторе всегда используются ссылки. Это обязательно. Параметр point — это объект, стоящий справа от оператора присваивания.
Деструктор класса
Деструктор класса — метод, вызываемый автоматически при уничтожении объекта. Это происходит, например, когда область видимости объекта заканчивается. Деструктор нужно писать явно, если в классе происходит выделение памяти. Соответственно, в деструкторе вам необходимо освободить все указатели.
Допустим в нашем танке есть экипаж, пусть это будет один объект типа Unit. При создании танка мы выделяем память под экипаж. В деструкторе нам нужно будет освободить память:
Имя деструктора совпадает с именем класса и перед ним ставится тильда
. Деструктор может быть только один.
Объектно-ориентированное программирование в C++ (ООП)
Теперь, когда мы представляем что такое классы и объекты, и умеем с ними работать, можно поговорить о объектно-ориентированном программировании. Сам по себе стиль ООП предполагает использование классов и объектов. Но помимо этого, у ООП есть ещё три характерные черты: инкапсуляция данных, наследование и полиморфизм.
Инкапсуляция данных — Encapsulation
Что означает слово Encapsulation? Корень — капсула. En — предлог в. Инкапсуляция — это буквально помещение в капсулу. Что помещается в капсулу? Данные и действия над ними: переменные и функции. Инкапсуляция — связывание данных и функций. Давайте ещё раз взглянем на класс Tank:
Собственно, здесь в класс Tank мы поместили переменную ammo и метод Attack. В методе Attack мы изменяем ammo. Это и есть инкапсуляция: члены класса (данные и методы) в одном месте.
В C++ есть ещё одно понятие, которое связано с инкапсуляцией — сокрытие данных. Сокрытие предполагает помещение данных (переменных класса) в область, в которой они не будут видимы в других частях программы. Для сокрытия используются спецификаторы доступа (access specifiers). Ключевые слова public и private и есть спецификаторы доступа. public говорит, что весь следующий блок будет видим за пределами определения класса. private говорит, что только методы класса имеют доступ к данным блока. Пример:
Здесь мы видим, что объект может получить доступ только к членам класса, находящимся в блоке public. При попытке обратиться к членам класса (и переменным, и методам) блока private, компилятор выдаст ошибку. При этом внутри любого метода класса мы можем обращаться к членам блока private. В методе Move мы изменяем скрытые переменные x и y.
Хороший стиль программирования в ООП предполагает сокрытие всех данных. Как тогда задавать значения скрытых данных и получать доступ к ним? Для этого используются методы setters и getters.
Setters and Getters
Setters и Getters сложно красиво перевести на русский. В своих уроках я буду использовать английские обозначения для них. Setter (set — установить) — это метод, который устанавливает значение переменной класса. Getter (get — получить) — метод, который возвращает значение переменной:
Имена не обязательно должны включать Set и Get. Использование setters и getters приводит к увеличению количества кода. Можно ли обойтись без инкапсуляции и объявить все данные в блоке public? Да, можно. Но данная экономия кода имеет свои негативные последствия. Мы будем подробно обсуждать данный вопрос, когда будем говорить об интерфейсах.
Следующая концепция ООП — наследование.
Наследование (Inheritance) в C++
Производный класс не может получить доступ к private членам. Поэтому в классе Unit используется спецификатор protected. Данный спецификатор разрешает доступ к данным внутри класса и внутри дочерних классов, private же разрешает доступ только в методах самого класса.
При наследовании производный класс имеет доступ ко всем членам (public и protected) базового класса. Именно поэтому мы можем вызвать метод Move для объекта типа Archer.
Обратите внимание, как происходит наследование. При определении дочернего класса, после имени ставится двоеточие, слово public и имя базового класса. В следущем уроке мы рассмотрим для чего здесь нужно слово public.
Полиморфизм (Polymorphism)
Наследование открывает доступ к полиморфизму. Poly — много, morph — форма. Это очень мощная техника, которую мы будем использовать постоянно.
Полиморфизм позволяет поместить в массив разные типы данных:
Мы создали массив указателей на Unit. Но C++ позволяет поместить в такой указатель и указатель на любой дочерний классс. Данная техника будет особенно полезна, когда мы изучим виртуальные функции.
Заключение
Классы позволяют легко моделировать лубую предметную область. Иногда лучше избежать использование ООП, но об этом мы поговорим в другой раз.
В следующем уроке мы познакомимся с более сложными концепциями, касающимися классов: виртуалье методы, шаблоны, статичные члены. Впоследствии мы увидим, как классы используютя в DirectX.
Единственное отличие между классом и структурой в C++: по умолчанию в структуре используется спецификатор доступа public, а в классе — private. Часто в коде вы будете видеть, что структуры используются без методов, чисто для описания каких-либо сущностей. Но это делать необязательно это всего лишь соглашение.