Вы находитесь на странице: 1из 36

Александр Загоруйко © 2020

Constructors
Начальная инициализация объектов

Предположим, существует некий класс


Cat, описывающий характеристики и
поведение домашнего животного.
Показывать состояние объекта этого
типа сразу же после создания объекта
не имеет смысла – будет видно, что
объект не настроен и не готов к работе:
Начальная инициализация объектов
class Cat {
public:
char* name;
char* breed;
void Print() {
cout << name << " " << breed<< ".";
}
};

// создание объекта в мейне:


Cat c;
c.Print(); // программа вылетает 
Рождение переменной
Когда создается
переменная, под неё
выделяется блок памяти – в
стеке или куче. Но само
значение переменной в этот
момент не определено, и
иногда его называют
«мусорным». То же самое
касается и объектов!
Чтение данных поля

П
Начальное состояние объекта

Разумеется, такое поведение объектов


нежелательно. Возьмём, скажем,
чайник. Этот объект обладает
свойством заполненности водой – от
нуля до максимального объема. Но на
полке магазина чайник всегда
продаётся пустой. Он «рождается»
таким.
Начальная инициализация объектов

К счастью, существует возможность


проинициализировать поля объекта
сразу же при его создании – например,
можно записать некоторые значения при
объявлении полей в классе. Проблема в
том, что теперь все объекты при
создании будут получать одинаковое
состояние. А что, если нужно будет
создать особенного кота?
Начальная инициализация объектов
class Cat
{
public:
char* name = "Ксеркс";
char* breed = "персидский";
void Print() {
cout << "кот по кличке " << name <<
" породы " << breed << ".\n";
}
};
Начальная инициализация объектов

Как вариант, в классе можно сделать


отдельный метод для записи всех
нужных значений в поля объекта.
Вполне приемлемо, но всё равно –
перед показом состояния объекта
придётся вызывать ещё какой-то
метод… а это долго и неудобно.
Начальная инициализация объектов

class Cat
{
public:
char* name;
char* breed;
void SetAll(char* n, char* b) {
name = n;
breed = b;
}
};
Как рождались структуры

Но, даже если дать пользователю такую функцию в


распоряжение, он может просто забыть её вызвать… Для
структур всегда есть возможность задать стартовое значение
самому – это потребует разобраться в устройстве структуры и
(что невозможно для класса) доступа к полям объекта.
Значит, нужен особый метод, который будет гарантированно
и автоматически вызываться в момент создания объекта.
Начальная инициализация объектов

Вот было бы здорово написать что-то


вроде
Cat c(“Барсик”, “мейн-кун”);
и сразу же получить готовый к
использованию объект…

Решение есть – нужно создать в классе


специальный метод-конструктор!
Конструктор с параметрами
class Cat
{
public:
char* name;
char* breed;
Cat(char* n, char* b)
{
name = n;
breed = b;
}
};
Отличия конструктора
Чтобы компилятор понял, какой из
методов класса играет роль конструктора,
его имя должно совпадать с именем
класса, а возвратный тип должен
отсутствовать.
Убедиться, что конструктор
действительно срабатывает, можно
добавлением cout в его тело – но только в
целях теста!
Определение понятия
Конструктор (construct - создавать) - это
специальный метод класса, объявленный с
таким же именем, как и сам класс. Для
конструктора НЕ указывается тип
возвращаемого значения, даже void!
Конструктор автоматически вызывается
при создании объекта, т.е. не нужно как-то
по-особенному его вызывать.
Основное назначение конструкторов -
инициализация объектов.
Автоматический вызов

Создание любого объекта компилятор


всегда сопровождает скрытым вызовом
конструктора:

Cat* c = new Cat();


// c->Cat::Cat();
Cat c2;
// c2.Cat::Cat();
Модификаторы конструкторов
Обычно, конструкторы находятся в секции
public. Впрочем, изредка можно встретить и
другие уровни доступа конструкторов (protected,
private).
С помощью параметров конструктору можно
передать любые данные, необходимые для
инициализации объектов класса. Допускается
перегрузка конструкторов, т.е. в классе может
быть несколько конструкторов с разными
сигнатурами.
Упс!

https://git.io/voq30
Теперь попробуем создать объект типа Cat
старым, привычным образом:

Cat* cat = new Cat(); // ERROR!


Причина проблемы
Дело в том, что до тех пор, пока
программист явно не определит в теле
класса хотя бы один конструктор,
компилятор предоставляет т.н. конструктор
по умолчанию, который не принимает
параметров, и ничего особенного не делает,
но зато позволяет создавать объекты по
упрощённому синтаксису:
Cat c; Cat* p = new Cat();
Добрый компилятор
Зачем компилятор вообще предоставляет конструктор
по умолчанию? Дело в том, что иногда для
инициализации приватных полей, либо при
наследовании (об этом речь пойдет позже),
компилятор добавляет в код конструктора
собственные команды. Поэтому, для классов, где мы
конструкторов не создаем, компилятор ВСЕГДА
добавляет конструктор без параметров с пустым
кодом внутри. Такой конструктор и называется
КОНСТРУКТОР ПО УМОЛЧАНИЮ. Если же мы задаём
свою версию конструктора, компилятор конструктор по
умолчанию уже не предоставляет.
Перегрузка конструкторов
Выход из сложившейся в примере ситуации –
добавить в класс явный конструктор без
параметров, и таким образом, выполнить
перегрузку. Более того, наличие
конструктора без параметров в классе
считается рекомендацией, т.к. это окажется
весьма полезным при работе с
наследованием.
Практика: делаем конструктор по умолчанию.
Перегрузка конструкторов
Итак, конструкторов в классе может быть
несколько. Каждому способу объявления
объекта класса должна соответствовать своя
версия конструктора класса. Если это не
будет обеспечено, то при компиляции
программы обнаружится ошибка. Главный
смысл перегрузки конструкторов состоит в
том, чтобы предоставить программисту
набор наиболее подходящих способов
инициализации объекта.
Перегрузка конструкторов
Наиболее распространённый вариант
перегрузки конструкторов – это конструктор
без параметров и один конструктор с
параметрами. Как правило, в программе
бывают необходимы оба эти вида. Хотя
конструктор можно перегружать столько раз,
сколько захотите, лучше не стоит этим
злоупотреблять. Конструктор стоит
перегружать лишь для наиболее часто
встречающихся ситуаций: https://git.io/voqsp
Делегирование конструкторов
Наверняка вы обратили внимание, что во всех версиях
конструктора выполняются примерно одни и те же действия:
name = …;
breed = …;
age = …;
Существует возможность оптимизировать такой код.
Вариант №1: https://git.io/voqGV
Вариант №2: из одного конструктора можно обратиться к
другому конструктору в этом же классе. Обычно, конструктор с
наибольшим количеством параметров назначается главным, а
все остальные конструкторы обращаются к нему:
https://git.io/voqZJ или даже https://git.io/voqZl
Инициализаторы для констант
class Cat {
char* name;
char* breed;
const int paws;
Cat(char* n, char* b) : paws(4) {
// ...
}
};
Выделение памяти
Если в составе класса есть поля-
указатели, то рекомендуется выделять
под них отдельные блоки памяти с
помощью конструкции new. Это позволит
каждому объекту класса работать со
своей копией данных, возможно
полученных извне (через конструкторы и
прочие методы) https://git.io/voqcz
https://git.io/voqC3
Возможны утечки

Однако, продвинутый способ выделения


памяти под поля объекта может повлечь
за собой «утечки памяти» в том случае,
если не будет особой функции для
освобождения ресурсов. А даже если она
есть – мы можем забыть её вызвать… 
Если мы говорим о классе, то нам нужен
метод, который будет гарантированно
вызываться в момент «смерти» объекта.
Деструкторы

Таким методом является деструктор,


который выполняет функцию,
противоположную функции конструктора.
Деструктор (destruct - разрушать) - это
специальный метод класса, который
автоматически вызывается при
уничтожении объекта - например, когда
объект выходит из области видимости.
Отличия деструктора
Имя деструктора состоит из тильды и имени
класса. Он не может принимать параметры, и
потому не может быть перегружен.
Вызывается компилятором автоматически –
когда заканчивается область видимости
локального объекта, или применяется
оператор delete для указателя на объект.
Если в классе нет явного деструктора,
компилятор сгенерирует свою пустую
версию.
Деструкторы
Деструктор может выполнять
любые задачи, в момент
удаления объекта. Например,
если в конструкторе была
динамически выделена
память с помощью new, то в
С++ деструктор обязан
высвободить эту память
перед удалением объекта
класса с помощью оператора
delete или функции free().
Жизненный цикл объекта
Время экспериментов

Давайте внедрим в конструктор и


деструктор класса Cat cout для тестовой
проверки их вызова. А потом создадим
функцию, которая принимает объект в
качестве параметра:

https://git.io/voqws
Конструктор копирования
Конструктор сработал только один раз, но
деструктор – дважды. Как же так?
Этот пример должен напомнить о том, что механизм
передачи параметров по значению гласит: внутри
функции будет создана новая переменная,
получающая своё значение извне. Значит, объект
some – это копия объекта c, поэтому деструктор
сработал отдельно для каждого из них. Но как был
создан объект some? При его создании компилятор
вызвал ещё один особый метод – конструктор
копирования.
Конструктор копирования
Когда в программе требуется создать
копию объекта, будет вызван именно
конструктор копирования. Так как такое
событие происходит часто, компилятор
всегда создает его, если он не определён
явно. В общем виде, он просто копирует
значения всех полей объекта – т.е.
проводит побитовое копирование
блока памяти, где размещён объект.
Случаи копирования

Копия объекта создается всего в трёх случаях:


-при передаче в функцию по значению
-при возврате объекта из функции
-при явном создании нового объекта на основе
другого
Так как объекты, как и экземпляры структур,
могут быть сколь угодно большими, их
рекомендуется передавать в функцию по ссылке.
Собственно, так и поступает сам конструктор
копирования.
Пример конструктора копирования

https://git.io/voqK5

Вам также может понравиться