C++. Классы
Время чтения: 7 минут
Основной особенностью C++ являются классы – без них, C++ практичеси ничем бы не отличался от Си.
Прежде чем мы перейдём к использованию классов, я расскажу откуда классы взялись – про объектно-ориентированное программирование.
Объектно-ориентированное программирование (ООП) – подход к написанию программ при котором:
- Программа представляется в виде набора объектов и связей между ними
- Все объекты являются экземплярами классов
- Классы образуют иерархию наследования (про это в другой статье)
Сейчас расскажу подробно что это всё значит.
Объект
В реальном мире мы с тобой постоянно взаимодействуем с какими-то объектами: кружка, стул, стол, телефон и т.д.
Как это всё может относиться к программированию?
До сих пор, твои программы представляли собой набор функций, которые вызывают друг друга в определённой последовательности (или просто одна функция). Этот подход называется процедурным – он хорошо себя показывает если нужно написать программу, которая выполняет только одну задачу.
Однако, если твоя программа делает сразу много задач, то тебе удобнее будет использовать ООП. С помощью этого подхода, ты сможешь представить свою программу как набор объектов, каждый из которых будет ответствененн только за свою область задач.
Вот пример простой программы “Список рецептов”, в которой можно создавать, изменять и удалять рецепты:
Всё что я сделал – это разнёс функции программы по разным объектам.
Чтобы добавить ещё больше контекста – вот примеры задач, которые я мог бы решать как программист:
- Нужно чтобы теперь файл базы данных лежал в другой папке:
- Залезу в код объекта “База Данных”:
- Поменяю путь до файла
- Залезу в код объекта “База Данных”:
- Нужно увеличить размер кнопок в интерфейсе на 50%:
- Залезу в код объекта “Пользовательский Интерфейс”:
- Поменяю свойство размера кнопок
- Залезу в код объекта “Пользовательский Интерфейс”:
- Нужно добавить для рецепта новое численное поле “Время приготовления”:
- Залезу в код объекта “База Данных”:
- Добавлю поле cooking_duration
- Залезу в код объекта “Пользовательский Интерфейс”:
- Добавлю новое численное поле ввода
- Подпишу его “Время приготовления”
- Добавлю дополнительный параметр cooking_duration, который отправляю в объект “Список Рецептов”
- Залезу в код объекта “Список Рецептов”:
- Добавлю логику получения значения cooking_duration из данных от объекта “Пользовательский Интерфейс”
- Изменю запросы на добавление/изменение/удаление в объект “База Данных”, чтобы они учитывали новое значение cooking_duration
- Залезу в код объекта “База Данных”:
В этом случае ООП не убавило работы – мне нужно писать столько же кода, чтобы выполнить задачу. Однако, теперь весь этот код сгруппирован в объектах, и мне намного легче удержать в голове взаимосвязи между тремя объектами, нежели множественные вызовы между десятками функций.
Это я в общих чертах описал чем отличается ООП от процедурного программирования, которым мы занимались до этого. Теперь рассмотрим классы.
Класс
Раньше – данные и функции для их обработки у тебя были разделены, а теперь класс, для удобства, объединяет их в одном месте.
Класс – это совокупность:
- Полей – переменных, определённых внутри класса
- Методов – функций, определённых внутри класса и работающих с полями
Объект – это экземпляр класса. Иными словами, объект – это переменная, содержащая в себе все поля и методы, описанные в классе.
Разберём на аналогии с кружкой кофе.
То есть если мы описали какой-то класс, мы не можем его использовать по назначению – нельзя выпить описание кружки кофе, можно выпить только экземпляр кружки кофе.
Давай создадим класс “Кофе” и попробуем что-то с ним сделать.
Для начала подключим необходимый нам заголовочный файл:
// Для вывода в консоль
#include <iostream>
// Для функций работы со строками
#include <cstring>
После этого опишем класс “Кофе”:
// Определяем новый класс с именем Coffee
class Coffee
{
// Про public мы поговорим в следующей статье, пока не обращай внимания
public:
// Конструктор - метод, вызываемый при создании объекта
Coffee(const char *type, int temperature, int volume)
{
m_temperature = temperature;
m_volume = volume;
// Выделяем память под строку
m_type = new char[strlen(type)];
// Копируем type в m_type
strcpy(m_type, type);
std::cout << "Constructed " << volume << "ml cup of " <<
(temperature > 50 ? "hot" : (temperature > 30 ? "warm" : "cold")) <<
" " << type << " coffee. " << std::endl;
}
// Деструктор - метод, вызываемый при удалении объекта
~Coffee()
{
// Освобождаем память под строку при удалении объекта
std::cout << "Destructed " << m_type << "." << std::endl;
delete[] m_type;
}
// Метод drink()
// Принимает на вход объём выпитого кофе и вычитает его из оставшегося объёма чашки
void drink(int ml_to_drink)
{
// Проверка, чтобы последний глоток кофе не вычел больше, чем осталось
ml_to_drink = m_volume > ml_to_drink ? ml_to_drink : m_volume;
m_volume -= ml_to_drink;;
std::cout << "Drank " << ml_to_drink << "ml of " << m_type << "." << std::endl;
}
// Поля класса - переменные, которые будут лежать внутри объекта
char *m_type; // Тип кофе
int m_temperature; // Температура
int m_volume; // Объём
}; // ОБРАТИ ВНИМАНИЕ - в конце определения класса стоит ";"
В C++ для работы со строками используют std::string, но я специально здесь сделал через Си-шные строки, чтобы показать удобство конструктора и деструктора.
В этом фрагменте кода я сделал:
- Класс – задал название класса Coffee
- Конструктор – особый метод, который вызывается при создании нового объекта
- Деструктор – особый метод, который вызывается при удалении объекта
- Обычный метод drink() – он будет вызываться у объекта, чтобы “выпить” его
- Поля – m_type, m_temperature, m_volume
Я дописал “m_” перед названиями полей (m – сокращение от Member, член класса), потому что это один из стилей их наименования. В этом случае всегда видно – обращаемся мы к полю класса или любой другой переменной. Рекомендую его придерживаться, но не настаиваю на этом.
Ну и давай теперь создадим несколько объектов этого класса, и повызываем у них метод drink():
// Комментариями я указал что будет выведено в консоль
int main()
{
Coffee c1("espresso", 90, 50);
// Created 50ml cup of hot espresso coffee.
Coffee c2("americano", 45, 200);
// Created 200ml cup of warm americano coffee.
Coffee *c3 = new Coffee("glasse", 10, 300);
// Created 300ml cup of cold glasse coffee.
c1.drink(30);
// Drank 30ml of espresso.
c1.drink(30);
// Drank 20ml of espresso.
c2.drink(500);
// Drank 200ml of americano.
c2.drink(10);
// Drank 0ml of americano.
c3->drink(25);
// Drank 25ml of glasse.
return 0;
// Destructed ameriacano.
// Destructed espresso.
}
Хочу обратить твоё внимание на два новых оператора: “new” и “delete”.
new
Для динамического выделения памяти под объект, я использовал оператор “new”.
Да, “new” это унарный оператор, который пишется перед типом. В Си у нас был только унарный оператор отрицания “!”.
В Си для выделения памяти мы пользовались функциями malloc() и calloc(), а в C++ вместо этого используют new – в чём разница?
malloc/calloc лишь выделял память – а теперь у нас появилась необходимость в вызове конструктора объекта.
new выполняет сразу две задачи:
- Выделение необходимого объёма памяти под объект
- Вызов конструктора, описанного в классе
Coffee *c3 = new Coffee("glasse", 10, 300);
// То же самое что и
c3 = (Coffee*) malloc(sizeof(Coffee)); // Выделение памяти
*c3 = Coffee("glasse", 10, 300); // Вызов конструктора для выделенной памяти
delete
И ещё одно – смотри, деструктор для “glasse” не вызвался. Это произошло из-за того, что он у нас хранится в указателе, который мы забыли подчистить.
Если же его подчистить, то всё встанет на свои места:
...
delete c3;
// Destructed glasse.
return 0;
// Destructed ameriacano.
// Destructed espresso.
}
В Си для освобождения памяти мы использовали free(), а тут какой-то delete – в чём разница?
delete – это ещё один оператор, который используется для вызова деструктора объекта и освобождения памяти.
delete c3;
// То же самое что и
c3->~Coffee(); // Вызов деструктора для выделенной памяти
free(c3); // Освобождение памяти
Если использовать только free, то деструктор не вызовется.
Заключение
Итого, мы изучили:
- Объектно-ориентированное программирование – описание программы как набор объектов
- Класс – совокупность полей и методов, работающих с ними
- Объект – экземпляр класса
- Поле – переменная внутри класса
- Метод – функция внутри класса
- Конструктор – особый метод, вызывающийся при создании объекта
- Деструктор – особый метод, вызывающийся при удалении объекта
- Операторы
- new – динамическое создание объекта (malloc + конструктор)
- delete – удаление динамически созданного объекта (деструктор + free)
Если что – пиши, я помогу и постараюсь объяснить лучше.
То, что мы с тобой сейчас изучили, тебе должно хватить для решения простых задач. Однако, это лишь вершина айсберга:
Не пугайся, мы не будем сразу разбирать весь “айсберг” – мы будем откалывать от него только самые полезные куски, и рассматривать их со всех сторон, пока полностью не разберёмся.
В следующей статье мы рассмотрим удобный способ вызова методов – переопределение операторов.