В этой статье мы рассмотрим, для чего в C++ нужны конструкторы и деструкторы.
Проблема с инициализацией
Как только мы начали работать с классами, то есть объединили данные и код в одном месте, то тут же возникает проблема с инициализацией переменных, то есть с заданием исходных значений.
Например, в языке С мы можем создать переменную и тут же задать ей значение:
1 |
int x = 5; |
Эта строка прямого действия — как она написана, так она и выполняется.
В C++ так сделать нельзя, потому что объявление класса — это просто описание свойств класса, а выполнение возможно только в экземпляре класса.
То есть программисту на C++ нужно все время помнить, на каком уровне что работает:
- или на уровне объявления класса,
- или на уровне экземпляра класса.
Например, наследование работает на уровне объявления класса, а присваивание значений — на уровне экземпляра класса.
Как же задать значение переменной? Первое, что приходит в голову — это создать экземпляр класса и затем задать значение свойству класса. То есть написать так.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> using namespace std; class Base { public: int x; }; int main() { Base b; b.x = 5; cout << b.x << endl; return 0; } |
Казалось бы, проблема решена и у нас получилось задать значение переменной. Но не все так просто. При создании экземпляров класса мы можем создавать их сколько угодно. Поэтому, если мы напишем.
1 |
Base b[10]; |
мы создадим 10 экземпляров класса и нам придется писать инициализацию для всех десяти переменных. Это при том, что у нас одна переменная, а если у нас их десяток, то присвоение начального значения становятся весьма трудоемким занятием.
Для решения этой проблемы в C++ добавлен такой инструмент как конструктор класса.
Конструктор класса
Конструктор — это функция, которая имеет то же имя, что и класс и вызывается каждый раз при создании класса. Если вы не определили ни одного конструктора, компилятор создаст конструктор по умолчанию, не имеющий параметров.
Конструктор работает на уровне экземпляра класса и предназначен для присвоения значений переменных. Конструктор не имеет типа возвращаемого значения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> using namespace std; class Base { public: Base() { x = 5; } int x; }; int main() { Base b[10]; cout << b[1].x << endl; return 0; } |
В этом пример мы объявили конструктор с именем Base, в котором и задали начальное значение. Хотя, конечно, по сравнению с C, где мы все это сделали одной строчкой, выглядит достаточно громоздко.
Динамические объекты в C++
Следующей важной особенностью C++ является работа с динамической памятью. В языке С работа с динамической памятью очень проста и использует всего две функции:
- malloc — резевирует память,
- free — освобождает память.
Например:
1 2 3 4 |
char *string; // задали указатель на строку string = malloc(1000); // получили память ... free( string ); // освободили память |
В C++ все намного сложнее. Для работы с динамической памятью вводится понятие динамический объект.
Динамический объект — это некоторая область в памяти, которая выделяется во время работы программы. В качестве объекта может выступать любая структура: от переменной до экземпляра класса.
Для создания нового объекта используется ключевое слово new.
1 |
int *px = new int; |
В этой строке создан новый динамический объект — целое число. Возможно создать объекта любого типа данных: int, float, double, char и т. д. Создадим динамический экземпляр класса:
1 |
Base *pBase = new Base; |
В этой строке мы создали динамический экземпляр класса. Только в этом случае мы должны ссылаться на свойства и методы через знак «->».
1 |
pBase->x |
Когда объект больше не нужен, то он удаляется ключевым словом delete. При использовании оператора delete для указателя, знак * не используется.
1 2 |
delete px; delete pBase; |
При этом если объект создан, но не уничтожен, то он будет находиться в памяти до завершения программы. Только после завершения программы вся память будет освобождена операционной системой. Поэтому нужно следить за тем, что освобождать все объекты, когда они больше не нужны.
Создание динамических объектов происходит в конструкторе класса. А где же проиходит их удаление? Для этого в классе есть специальная функция — деструктор.
Деструктор класса
Деструктор — это функция, которая имеет то же имя, что и класс, но со знаком «~» (тильда) в начале. В деструкторе нужно уничтожить все динамические объекты, которые были созданы в конструкторе.
При объявлении деструкторов действуют несколько правил. Деструкторы:
- Не могут иметь аргументов.
- Не могут иметь возвращаемого типа (включая void).
- Не могут возвращать значение с помощью оператора return.
Теперь напишем пример, в котором используются все функции, которые мы узнали:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#include <iostream> using namespace std; class Base { public: Base() { px = new int(5); pa = new int[7] { 9, 7, 5, 3, 1, 4 }; } ~Base() { delete px; delete []pa; } int *px; int *pa; }; int main() { Base *pBase = new Base; cout << pBase->pa[0] << endl; return 0; } |
В этом примере мы создали:
- динамическую переменную px,
- динамический массив pa,
- динамический экземпляр класса pBase.
Только надо указать компилятору, что используется C++ 11, раньше нельзя было задать элементы массива при создании динамического массива.
Проблема утечки памяти
Работа с динамическими объектами в C++ породило проблему утечки памяти.
В языке Си в силу простоты вызова обычно программисты используют схему:
- Взял память,
- Использовал,
- Освободил.
Ошибиться, конечно, можно и здесь, но в целом схема простая и ошибку найти не сложно.
Как только запускается программа на C++, то на основе объявлений классов начинают создаваться или уничтожаться разнообразные объекты. При этом если объект был использован, но память не освободил, то при каждом создании этого объекта программа начинает забирать все больше памяти. Постепенно программа забирает всю оперативную память и операционная систем начинает запускать процесс свопинга — подключения дисковой памяти, что вызывает резкое торможение всей системы.
С точки зрения пользователя — это выглядит так. Сначала программа работала быстро, а потом начинает замедляться и замедляться. И помогает только закрытие программы и открытие ее снова.
Проблема утечки памяти — это одна из самых распространенных ошибок в языке C++. Как выразился автор книги «Думай как программист» Антон Спрол: «Управление временем существования переменной — это бич каждого программиста С++».
Поэтому работа с динамическими объектами в C++ требует особой внимательности.