# Глава 5.5. Классы и структуры
C++ появился как надстройка над Си под названием «Си с классами». А от Си он унаследовал структуры. В этой главе мы разберемся, что такое классы и структуры, и чем они отличаются.
## Классы
[Класс](https://en.cppreference.com/w/cpp/language/class) (class) — это составной тип данных. Он позволяет:
- Объединять данные и методы их обработки.
- Хранить состояние.
- Описывать абстракции и предоставлять интерфейс для работы с ними.
- Ограничивать доступ к полям и методам, то есть [инкапсулировать](https://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%BA%D0%B0%D0%BF%D1%81%D1%83%D0%BB%D1%8F%D1%86%D0%B8%D1%8F_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)) внутреннюю реализацию.
- Автоматически управлять ресурсами (об этом чуть позже).
Верхнеуровнево класс выглядит так:
```cpp
class имя_класса
{
// содержимое: поля и методы
};
```
Данные класса хранятся в его полях. {#block-class-message}
```cpp
class Message
{
public:
std::string id;
std::string text;
};
```
[Спецификатор доступа](https://en.cppreference.com/w/cpp/language/access) `public` означает, что перечисленные после него поля и методы составляют его публичный интерфейс и открыты пользователям класса.
Методы класса выглядят как функции, определённые внутри класса. Они имеют доступ ко всем его полям и методам. Методы могут иметь перегрузки. {#block-class-unixtimestamp}
```cpp {.example_for_playground .example_for_playground_009}
class UnixTimestamp
{
public:
void show_days()
{
const std::time_t seconds_in_day = 24 * 60 * 60;
std::println("{} days since 01.01.1970", seconds / seconds_in_day);
}
std::time_t seconds = 0;
};
```
Для хранения временной метки формата [Unix time](https://en.wikipedia.org/wiki/Unix_time) мы воспользовались [типом std::time_t](https://en.cppreference.com/w/cpp/chrono/c/time_t). Его реализация зависит от компилятора, но чаще всего это знаковое целое число. Оно хранит количество секунд, прошедших с 00:00 1 января 1970 года (UTC).
Обратите внимание, что поле `seconds` мы инициализировали нулём, то есть явно задали его значение по умолчанию:
```cpp
std::time_t seconds = 0;
```
Явно инициализировать поля простых типов значениями можно _и нужно._
Для обращения к полям и методам _объекта класса_ используется оператор доступа к элементу `.`:
```cpp {.example_for_playground .example_for_playground_010}
UnixTimestamp ts;
ts.seconds = 1153044000;
ts.show_days();
```
Поля и методы можно сделать внутренними — доступными только из других методов, но не снаружи. Для этого предназначен спецификатор доступа `private`. Все, что перечислено после него, составляет внутреннюю реализацию класса.
Рассмотрим класс `Task`, описывающий задачу в типичном таск-трекере. У него есть методы для логирования времени работы над задачей и обновления её статуса. Поля, хранящие время и статус, сделаны приватными. В некоторых код-стайлах приватные поля именуются с префиксом `m_` (member). {#block-class-task}
```cpp {.example_for_playground .example_for_playground_011}
enum class State { Todo = 1, InProgress = 2, Done = 3 };
class Task
{
public:
bool log_work_hours(std::size_t hours)
{
if (m_state == State::Todo)
m_state = State::InProgress;
if (m_state == State::InProgress)
m_workHours += hours;
return m_state == State::InProgress;
}
bool update_state(State new_state)
{
if (new_state > m_state)
m_state = new_state;
return m_state == new_state;
}
void show_work_hours() { std::println("{}", m_workHours); }
private:
State m_state = State::Todo;
std::size_t m_workHours = 0;
};
```
А теперь поработаем с объектом класса `Task`:
```cpp {.example_for_playground .example_for_playground_012}
Task t;
t.update_state(State::InProgress);
t.log_work_hours(5);
t.show_work_hours();
```
Напишите класс `Device` с публичными методами: {.task_text}
`void start()` — помечает устройство как запущенное. Сохраняет время включения в секундах (тип `std::time_t`). {.task_text}
`void stop()`— помечает устройство как выключенное. По умолчанию (если метод `start()` ни разу не вызывался) устройство считается выключенным. {.task_text}
`void set_latest_healthcheck()` — сохраняет время последнего хэлсчека — аудита «здоровья». Если на этот момент устройство выключено, кидает исключение `std::logic_error`. {.task_text}
`bool is_active()` — возвращает `true`, если устройство включено и с момента последнего хэлсчека прошло не больше 1 минуты. {.task_text}
`std::time_t uptime()` — возвращает время работы. Если устройство выключено, возвращает 0. {.task_text}
Для получения текущего времени в проекте уже есть функция `std::time_t get_cur_time()`. Используйте её. {.task_text}
Чтобы хранить состояние устройства, заведите 3 приватных поля: `is_on` — включено устройство или нет; `time_started` — время запуска; `time_checked` — время последнего хэлсчека. При объявлении полей обязательно проинициализируйте их. Значение по умолчанию `is_on` равно `false` (устройство должно быть выключено до явного вызова `start()`); а время запуска и время хэлсчека по умолчанию должны быть равны 0. {.task_text}
```cpp {.task_source #cpp_chapter_0055_task_0060}
```
Метод `start()` должен устанавливать поле `is_on` в `true`, а `time_started` в `get_cur_time()`. Метод `stop()` должен устанавливать `is_on` в `false`. Метод `set_latest_healthcheck()` должен проверять `is_on` и бросать `std::logic_error`. А если устройство включено, устанавливать `time_checked` в `get_cur_time()`. Метод `is_acitve()` должен возвращать результат выражения `is_on && get_cur_time() < time_checked + 60`. А метод `uptime()` — результат выражения `is_on ? get_cur_time() - time_started : 0`. {.task_hint}
```cpp {.task_answer}
class Device
{
public:
void start()
{
is_on = true;
time_started = get_cur_time();
}
void stop()
{
is_on = false;
}
void set_latest_healthcheck()
{
if (!is_on)
throw std::logic_error("device is off");
time_checked = get_cur_time();
}
bool is_active()
{
return is_on && get_cur_time() < time_checked + 60;
}
std::time_t uptime()
{
return is_on ? get_cur_time() - time_started : 0;
}
private:
std::time_t time_checked = 0;
std::time_t time_started = 0;
bool is_on = false;
};
```
### Конструкторы и деструкторы
У классов есть особые методы — конструкторы и деструкторы. Конструкторы нужны для корректного создания объектов. А деструкторы — для их разрушения. Из конструкторов и деструкторов, как и из обычных методов, можно обращаться к полям класса, вызывать методы и свободные функции.
Базовые факты о конструкторах:
- Конструктор нужен для инициализации состояния объекта.
- Имя конструктора совпадает с именем класса.
- Возвращаемый тип конструктора не указывается. Он ничего не возвращает. Но может бросить исключение.
- Класс может иметь несколько конструкторов: допустима их перегрузка.
- Конструктор, не принимающий параметров, называется конструктором по умолчанию.
- У класса может не быть явно добавленных конструкторов. Тогда при создании объекта вызывается конструктор по умолчанию. Он в свою очередь вызывает конструкторы по умолчанию для полей класса.
- Конструктор вызывается неявно при создании объекта, но может быть вызван и напрямую.
Базовые факты о деструкторах:
- Деструктор нужен для освобождения используемых объектом ресурсов.
- Имя деструктора совпадает с именем класса, перед которым ставится символ `~`. Например, `~Device()`.
- Как и у конструктора, у деструктора не указывается возвращаемый тип.
- Если деструктор бросит исключение, в общем случае программа аварийно завершится. Позже вы узнаете о причинах такого поведения и о способах его обхода.
- Деструктор не принимает аргументов, поэтому его перегрузка невозможна.
- Если деструктор не указан явно, для класса создаётся деструктор по умолчанию. Он вызывает деструкторы полей класса.
- Деструктор срабатывает в момент удаления объекта. Практически никогда не требуется вызывать его напрямую.
- Деструкторы вызываются в порядке, обратном конструированию.
```cpp {.example_for_playground .example_for_playground_013}
class File
{
public:
// Конструктор по умолчанию
File() { }
// Конструктор с параметром
File(std::string path) { }
// Деструктор
~File(){ }
};
int main()
{
// Создание объекта с помощью конструктора по умолчанию
File f1;
// Создание объекта с помощью конструктора с параметром
File f2("/tmp/dump.csv");
// При выходе из main() для f2 и f1 будут вызваны
// деструкторы, причём именно в таком порядке
}
```
### Идиома RAII {#block-raii}
Конструкторы и деструкторы позволяют в C++ реализовывать идиому [RAII.](https://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5_%D1%80%D0%B5%D1%81%D1%83%D1%80%D1%81%D0%B0_%D0%B5%D1%81%D1%82%D1%8C_%D0%B8%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F) Resource Acquisition Is Initialization (получение ресурса есть инициализация) — это подход для автоматического управления ресурсами, требующими парных действий. Например, выделение и освобождение памяти, подключение и отключение от БД, открытие и закрытие файла.
Получение ресурса реализуется в конструкторе, а освобождение — в деструкторе. Когда объект разрушается, ресурс освобождается автоматически. Разработчику не нужно заботиться об этом в каждой из веток кода.
В проекте есть класс `DBConn` для работы с БД. Этот класс — обёртка над старым кодом на Си. Он реализует методы для подключения, отключения и выполнения запроса к БД. {.task_text}
Перепишите `DBConn` в соответствии с идиомой RAII. Заведите конструктор, внутри которого происходит подключение к БД. В случае неудачи конструктор должен бросать исключение `std::runtime_error`. Отключение от БД должно быть в деструкторе. {.task_text}
Затем перепишите в соответствии с этими изменениями класса функцию `handle_metrics()`. Перехватывать в ней исключения не нужно: они обрабатываются в коде, вызывающем `handle_metrics()`. {.task_text}
```cpp {.task_source #cpp_chapter_0055_task_0070}
class DBConn
{
public:
bool open(std::string conn_str)
{
if (is_open())
return false;
// Метод строки c_str() возвращает
// указатель на строку в сишном стиле.
// Про указатели вы узнаете позже.
m_handle = open_db(conn_str.c_str());
return is_open();
}
// После переписывания класса на RAII этот
// метод больше не будет нужен.
bool is_open()
{
return m_handle != INVALID_DB_HANDLE;
}
// Этот метод тоже будет не нужен.
// Соединение будет закрывать деструктор.
void close()
{
if (is_open())
{
close_db(m_handle);
m_handle = INVALID_DB_HANDLE;
}
}
bool exec(std::string query)
{
if (!is_open())
return false;
return exec_db_query(m_handle, query.c_str());
}
private:
db_handle m_handle = INVALID_DB_HANDLE;
};
// Перехватывать исключения в этом методе не нужно.
// Пусть они пробрасываются дальше.
void handle_metrics()
{
DBConn db_conn;
if (!db_conn.open("postgresql://user:secret@localhost"))
return;
if (!db_conn.exec("select * from metrics"))
{
// Упс! Здесь забыт вызов db_conn.close().
// Открытое соединение продолжит висеть.
// С RAII подобные ошибки исключены.
return;
}
db_conn.close();
}
```
Заведите конструктор `DBConn(std::string conn_str)`, открывающий подключение к БД. Закрывайте подключение в деструкторе `~DBConn()`. {.task_hint}
```cpp {.task_answer}
class DBConn
{
public:
DBConn(std::string conn_str)
{
m_handle = open_db(conn_str.c_str());
if (m_handle == INVALID_DB_HANDLE)
throw std::runtime_error("Couldn't connect");
}
~DBConn()
{
close_db(m_handle);
}
bool exec(std::string query)
{
return exec_db_query(m_handle, query.c_str());
}
private:
db_handle m_handle = INVALID_DB_HANDLE;
};
void handle_metrics()
{
DBConn db_conn("postgresql://user:secret@localhost");
db_conn.exec("select * from metrics");
}
```
Идиома RAII — один из столпов современного C++. Мы не раз к ней вернёмся.
## Структуры
Помимо классов в C++ есть [структуры](https://en.cppreference.com/w/c/language/struct) (struct). Они очень похожи. Но поля и методы класса по умолчанию приватные (private). А у структуры — публичные (public):
```cpp {.example_for_playground .example_for_playground_014}
struct UserMessage
{
std::size_t id = 0;
std::size_t time_created = 0;
std::string text;
};
UserMessage msg;
msg.text = "C++: Simula in wolf’s clothing";
```
Когда лучше использовать структуры, а когда — классы?
Структуры подходят для группировки данных, каждое поле которых изменяется отдельно от других без нарушения целостности объекта. Все поля таких структур остаются открытыми. И зачастую такие структуры не имеют методов.
Например, координата точки на плоскости состоит из двух значений. Изменение любого из них не приводит координату в неконсистентное состояние.
```cpp {.example_for_playground .example_for_playground_015}
struct Point
{
double x = 0.0;
double y = 0.0;
};
Point p;
p.x = 10.1;
p.y = -2.0;
```
Если же изменение полей способно сломать состояние объекта, то вместо структур применяются классы. Поля делаются закрытыми, а их чтение и изменение реализуется через методы.
В этой главе мы обсудили классы и привели три простых примера: [Message,](/courses/cpp/chapters/cpp_chapter_0055/#block-class-message) [UnixTimestamp,](/courses/cpp/chapters/cpp_chapter_0055/#block-class-unixtimestamp) [Task.](/courses/cpp/chapters/cpp_chapter_0055/#block-class-task) Перечислите те из них, которые логичнее было бы сделать структурами. {.task_text}
```consoleoutput {.task_source #cpp_chapter_0055_task_0080}
```
Только в классе `Task` изменение одного из полей на произвольное значение приведёт объект в неконсистентное состояние. {.task_hint}
```cpp {.task_answer}
Message UnixTimestamp
```
----------
## Резюме
- Классы позволяют описывать абстракции, отделять интерфейс для работы с ними от внутренней реализации, объединять данные и методы их обработки.
- Конструкторы и деструкторы классов позволяют реализовать идиому RAII.
- Структуры подходят для группировки полей, изменение которых по отдельности не приведёт объект в неконсистентное состояние.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!