+--------------------------+
|.------------------------.|
|| kee_reel@blog:~$ cd    ||
|| c c++ python           ||
|| opengl sql сети        ||
||                        ||
|| мой_проект     обо_мне ||
|.------------------------.|
+-::--------------------::-+
.--------------------------.
 // /ooooooooooooooooooooo\\ \\ 
 // /ooooooooooooooooooooooo\\ \\ 
//------------------------------\\
\\------------------------------//

C++. Классы

Основной особенностью 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 = (char*)malloc(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;
		free(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)

Если что – пиши, я помогу и постараюсь объяснить лучше.

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

C++ айсберг

Не пугайся, мы не будем сразу разбирать весь “айсберг” – мы будем откалывать от него только самые полезные куски, и рассматривать их со всех сторон, пока полностью не разберёмся.

В следующей статье мы рассмотрим удобный способ вызова методов – переопределение операторов.