Для чего нужны конструкторы и деструкторы в с
В этой статье мы рассмотрим, для чего в C++ нужны конструкторы и деструкторы.
Проблема с инициализацией
Как только мы начали работать с классами, то есть объединили данные и код в одном месте, то тут же возникает проблема с инициализацией переменных, то есть с заданием исходных значений.
Например, в языке С мы можем создать переменную и тут же задать ей значение:
Эта строка прямого действия — как она написана, так она и выполняется.
В C++ так сделать нельзя, потому что объявление класса — это просто описание свойств класса, а выполнение возможно только в экземпляре класса.
То есть программисту на C++ нужно все время помнить, на каком уровне что работает:
- или на уровне объявления класса,
- или на уровне экземпляра класса.
Например, наследование работает на уровне объявления класса, а присваивание значений — на уровне экземпляра класса.
Как же задать значение переменной? Первое, что приходит в голову — это создать экземпляр класса и затем задать значение свойству класса. То есть написать так.
using namespace std;
Казалось бы, проблема решена и у нас получилось задать значение переменной. Но не все так просто. При создании экземпляров класса мы можем создавать их сколько угодно. Поэтому, если мы напишем.
Base b[10];
мы создадим 10 экземпляров класса и нам придется писать инициализацию для всех десяти переменных. Это при том, что у нас одна переменная, а если у нас их десяток, то присвоение начального значения становятся весьма трудоемким занятием.
Для решения этой проблемы в C++ добавлен такой инструмент как конструктор класса.
Конструктор класса
Конструктор — это функция, которая имеет то же имя, что и класс и вызывается каждый раз при создании класса. Если вы не определили ни одного конструктора, компилятор создаст конструктор по умолчанию, не имеющий параметров.
Конструктор работает на уровне экземпляра класса и предназначен для присвоения значений переменных. Конструктор не имеет типа возвращаемого значения.
using namespace std;
В этом пример мы объявили конструктор с именем Base, в котором и задали начальное значение. Хотя, конечно, по сравнению с C, где мы все это сделали одной строчкой, выглядит достаточно громоздко.
Динамические объекты в C++
Следующей важной особенностью C++ является работа с динамической памятью. В языке С работа с динамической памятью очень проста и использует всего две функции:
- malloc — резевирует память,
- free — освобождает память.
char *string; // задали указатель на строку
string = malloc(1000); // получили память
free( string ); // освободили память
В C++ все намного сложнее. Для работы с динамической памятью вводится понятие динамический объект .
Динамический объект — это некоторая область в памяти, которая выделяется во время работы программы. В качестве объекта может выступать любая структура: от переменной до экземпляра класса.
Для создания нового объекта используется ключевое слово new .
int *px = new int;
В этой строке создан новый динамический объект — целое число. Возможно создать объекта любого типа данных: int, float, double, char и т. д. Создадим динамический экземпляр класса:
Base *pBase = new Base;
В этой строке мы создали динамический экземпляр класса. Только в этом случае мы должны ссылаться на свойства и методы через знак «->».
Когда объект больше не нужен, то он удаляется ключевым словом delete . При использовании оператора delete для указателя, знак * не используется.
delete pBase;
При этом если объект создан, но не уничтожен, то он будет находиться в памяти до завершения программы. Только после завершения программы вся память будет освобождена операционной системой. Поэтому нужно следить за тем, что освобождать все объекты, когда они больше не нужны.
Создание динамических объектов происходит в конструкторе класса. А где же проиходит их удаление? Для этого в классе есть специальная функция — деструктор .
Деструктор класса
Деструктор — это функция, которая имеет то же имя, что и класс, но со знаком «~» (тильда) в начале. В деструкторе нужно уничтожить все динамические объекты, которые были созданы в конструкторе.
При объявлении деструкторов действуют несколько правил. Деструкторы:
- Не могут иметь аргументов.
- Не могут иметь возвращаемого типа (включая void).
- Не могут возвращать значение с помощью оператора return.
Теперь напишем пример, в котором используются все функции, которые мы узнали:
using namespace std;
px = new int(5);
Base *pBase = new Base;
В этом примере мы создали:
- динамическую переменную px,
- динамический массив pa,
- динамический экземпляр класса pBase.
Только надо указать компилятору, что используется C++ 11, раньше нельзя было задать элементы массива при создании динамического массива.
Проблема утечки памяти
Работа с динамическими объектами в C++ породило проблему утечки памяти.
В языке Си в силу простоты вызова обычно программисты используют схему:
- Взял память,
- Использовал,
- Освободил.
Ошибиться, конечно, можно и здесь, но в целом схема простая и ошибку найти не сложно.
Как только запускается программа на C++, то на основе объявлений классов начинают создаваться или уничтожаться разнообразные объекты. При этом если объект был использован, но память не освободил, то при каждом создании этого объекта программа начинает забирать все больше памяти. Постепенно программа забирает всю оперативную память и операционная систем начинает запускать процесс свопинга — подключения дисковой памяти, что вызывает резкое торможение всей системы.
С точки зрения пользователя — это выглядит так. Сначала программа работала быстро, а потом начинает замедляться и замедляться. И помогает только закрытие программы и открытие ее снова.
Проблема утечки памяти — это одна из самых распространенных ошибок в языке C++. Как выразился автор книги «Думай как программист» Антон Спрол: «Управление временем существования переменной — это бич каждого программиста С++».
Поэтому работа с динамическими объектами в C++ требует особой внимательности.
Нигде не утверждается, что объект должен быть инициализирован, и программист может забыть инициализировать его или сделать это дважды.
ООП дает возможность программисту описать функцию, явно предназначенную для инициализации объектов. Поскольку такая функция конструирует значения данного типа, она называется конструктором . Конструктор всегда имеет то же имя, что и сам класс и никогда не имеет возвращаемого значения. Когда класс имеет конструктор, все объекты этого класса будут проинициализированы.
Если конструктор требует аргументы, их следует указать:
date today = date(6,4,2014); // полная форма
date xmas(25,12,0); // сокращенная форма
// date my_burthday; // недопустимо, опущена инициализация
Если необходимо обеспечить несколько способов инициализации объектов класса, задается несколько конструкторов:
class date <
int month, day, year;
public :
date( int , int , int ); // день месяц год
date( char *); // дата в строковом представлении
date(); // дата по умолчанию: сегодня
>;
Конструкторы подчиняются тем же правилам относительно типов параметров, что и перегруженные функции. Если конструкторы существенно различаются по типам своих параметров, то компилятор при каждом использовании может выбрать правильный:
Одним из способов сократить количество перегруженных функций (в том числе и конструкторов) является использование значений по умолчанию.
Конструктор по умолчанию
Конструктор, не требующий параметров, называется конструктором по умолчанию . Это может быть конструктор с пустым списком параметров или конструктор, в котором все аргументы имеют значения по умолчанию.
Конструкторы могут быть перегруженными, но конструктор по умолчанию может быть только один.
class date
int month, day, year;
public :
date( int , int , int );
date( char *);
date(); // конструктор по умолчанию
>;
При создании объекта вызывается конструктор, за исключением случая, когда объект создается как копия другого объекта этого же класса, например:
Однако имеются случаи, в которых создание объекта без вызова конструктора осуществляется неявно:
- формальный параметр – объект, передаваемый по значению, создается в стеке в момент вызова функции и инициализируется копией фактического параметра;
- результат функции – объект, передаваемый по значению, в момент выполнения оператора return копируется во временный объект, сохраняющий результат функции.
Во всех этих случаях транслятор не вызывает конструктора для вновь создаваемого объекта:
- date2 в приведенном определении;
- для создаваемого в стеке формального параметра;
- для временного объекта, сохраняющего значение, возвращаемое функцией.
Вместо этого в них копируется содержимое объекта-источника:
- date1 в приведенном примере;
- фактического параметра;
- объекта-результата в операторе return .
Конструктор копии
Как правило, при создании нового объекта на базе уже существующего происходит поверхностное копирование, то есть копируются те данные, которые содержит объект-источник. При этом если в объекте-источнике имеются указатели на динамические переменные и массивы, или ссылки, то создание копии объекта требует обязательного дублирования этих объектов во вновь создаваемом объекте. С этой целью вводится конструктор копии, который автоматически вызывается во всех перечисленных случаях. Он имеет единственный параметр — ссылку на объект-источник:
Деструкторы
Определяемый пользователем класс имеет конструктор, который обеспечивает надлежащую инициализацию. Для многих типов также требуется обратное действие. Деструктор обеспечивает соответствующую очистку объектов указанного типа. Имя деструктора представляет собой имя класса с предшествующим ему знаком «тильда» ~ . Так, для класса X деструктор будет иметь имя ~X() . Многие классы используют динамическую память, которая выделяется конструктором, а освобождается деструктором.
class date
int day, year;
char *month;
public :
date( int d, char * m, int y)
day = d;
month = new char [strlen(m)+1];
strcpy_s(month, strlen(m)+1,m);
year = y;
>
~date() < delete [] month; >// деструктор
>;
Поля, имеющие тип класса
Пусть имеется класс vect , реализующий защищенный массив, и необходимо хранить несколько значений для каждого такого массива: возраст, вес и рост группы лиц. Группируем 3 массива внутри нового класса.
Конструктор нового класса имеет пустое тело и список вызываемых конструкторов класса vect , перечисленных после двоеточия (:) через запятую (,). Они выполняются с целым аргументом i , создавая 3 объекта класса vect: a, b, c .
Конструкторы членов класса всегда выполняются до конструктора класса, в котором эти члены описаны. Порядок выполнения конструкторов для членов класса определяется порядком объявления членов класса. Если конструктору члена класса требуются аргументы, этот член с нужными аргументами указывается в списке инициализации. Деструкторы вызываются в обратном порядке.
Здравствуйте, у меня такой вопрос. В коде я не нашел сам деструктор. Он создается и запускается сам после выхода из блока main ?
Наследование — это механизм создания нового класса на основе уже существующего. При этом к существующему классу могут быть добавлены новые элементы (данные и функции), либо существующие функции могут быть изменены. Основное назначение механизма наследования — повторное использование кодов, так как большинство используемых типов данных являются вариантами друг друга, и писать для каждого свой класс нецелесообразно.
Объекты разных классов и сами классы могут находиться в отношении наследования, при котором формируется иерархия объектов, соответствующая заранее предусмотренной иерархии классов.
Иерархия классов позволяет определять новые классы на основе уже имеющихся. Имеющиеся классы обычно называют базовыми (иногда порождающими), а новые классы, формируемые на основе базовых, – производными (порожденными, классами-потомками или наследниками).
При наследовании некоторые имена методов (функций-членов) и полей (данных-членов) базового класса могут быть по-новому определены в производном классе. В этом случае соответствующие компоненты базового класса становятся недоступными из производного класса. Для доступа из производного класса к компонентам базового класса, имена которых повторно определены в производном, используется операция разрешения контекста ::
Для порождения нового класса на основе существующего используется следующая общая форма
При объявлении порождаемого класса МодификаторДоступа может принимать значения public , private , protected либо отсутствовать, по умолчанию используется значение private . В любом случае порожденный класс наследует все члены базового класса, но доступ имеет не ко всем. Ему доступны общие ( public ) члены базового класса и недоступны частные ( private ).
Для того, чтобы порожденный класс имел доступ к некоторым скрытым членам базового класса, в базовом классе их необходимо объявить со спецификацией доступа защищенные ( protected ).
Члены класса с доступом protected видимы в пределах класса и в любом классе, порожденном из этого класса.
Общее наследование
При общем наследовании порожденный класс имеет доступ к наследуемым членам базового класса с видимостью public и protected . Члены базового класса с видимостью private – недоступны.
Спецификация доступа | внутри класса | в порожденном классе | вне класса |
private | + | — | — |
protected | + | + | — |
public | + | + | + |
Общее наследование означает, что порожденный класс – это подтип базового класса. Таким образом, порожденный класс представляет собой модификацию базового класса, которая наследует общие и защищенные члены базового класса.
Порожденный класс наследует все данные класса student (строка 13), имеет доступ к protected и public — членам базового класса. В новом классе добавлено два поля данных (строки 16, 17), и порожденный класс переопределяет функцию print() (строки 20, 39-43).
Конструктор для базового класса вызывается в списке инициализации (строка 29).
Но что происходит, когда мы присваиваем указателю класса student ссылку на объект класса grad_student (строка 55)? В этом случае происходит преобразование указателей, и в строке 56 вызывается уже функция print() класса student .
Указатель на порожденный класс может быть неявно передан в указатель на базовый класс. А указатель на порожденный класс может указывать только на объекты порожденного класса. То есть обратное преобразование недопустимо
Неявные преобразования между порожденным и базовым классами называются предопределенными стандартными преобразованиями :
- объект порожденного класса неявно преобразуется к объекту базового класса.
- ссылка на порожденный класс неявно преобразуется к ссылке на базовый класс.
- указатель на порожденный класс неявно преобразуется к указателю на базовый класс.
Частное наследование
Порожденный класс может быть базовым для следующего порождения. При порождении private наследуемые члены базового класса, объявленные как protected и public , становятся членами порожденного класса с видимостью private . При этом члены базового класса с видимостью public и protected становятся недоступными для дальнейших порождений. Цель такого порождения — скрыть классы или элементы классов от использования их в дальнейших порождениях. При порождении private не выполняются предопределенные стандартные преобразования:
Однако порождение private позволяет отдельным элементам базового класса с видимостью public и protected сохранить свою видимость в порожденном классе. Для этого необходимо
- в части protected порожденного класса указать те наследуемые члены базового класса с видимостью protected , уточненные именем базового класса, для которых необходимо оставить видимость protected и в порожденном классе;
- в части public порожденного класса указать те наследуемые члены базового класса с видимостью public , уточненные именем базового класса, для которых необходимо оставить видимость public и в порожденном классе.
class X
private :
int n;
protected :
int m;
char s;
public :
void func( int );
>;
class Y : private X
private :
.
protected :
.
X::s;
public :
.
X::func();
>;
Возможен и третий вариант наследования – с использованием модификатора доступа protected .
Доступ к элементам базового класса из производного класса, в зависимости от модификатора наследования:
Конструкторы и деструкторы при наследовании
Как базовый, так и производный классы могут иметь конструкторы и деструкторы.
Если и у базового и у производного классов есть конструкторы и деструкторы, то конструкторы выполняются в порядке наследования, а деструкторы – в обратном порядке. То есть если А – базовый класс, В – производный из А , а С – производный из В ( А-В-С ), то при создании объекта класса С вызов конструкторов будет иметь следующий порядок:
- конструктор класса А
- конструктор класса В
- конструктор класса С .
Вызов деструкторов при удалении этого объекта произойдет в обратном порядке:
- деструктор класса С
- деструктор класса В
- деструктор класса А .
Поскольку базовый класс «не знает» о существовании производного класса, любая инициализация выполняется в нем независимо от производного класса, и, возможно, становится основой для инициализации, выполняемой в производном классе. Поскольку базовый класс лежит в основе производного, вызов деструктора базового класса раньше деструктора производного класса привел бы к преждевременному разрушению производного класса.
Конструкторы могут иметь параметры. При реализации наследования допускается передача параметров для конструкторов производного и базового класса. Если параметрами обладает только конструктор производного класса, то аргументы передаются обычным способом. При необходимости передать аргумент из производного класса конструктору родительского класса используется расширенная запись конструктора производного класса:
КонструкторПроизводногоКласса (СписокФормальныхАргументов)
: КонструкторБазовогоКласса (СписокФактическихАргументов)
< // тело конструктора производного класса >
Для базового и производного классов допустимо использование одних и тех же аргументов. Возможно, списки аргументов конструкторов производного и базового классов будут различны.
Конструктор производного класса не должен использовать все аргументы, часть предназначены для передачи в базовый класс (строка 29, см. код выше). В расширенной форме объявления конструктора производного класса описывается вызов конструктора базового класса.
Здравствуйте! а подскажите, пожалуйста, как я могу объявить неполный наследуемый класс? Например, у меня есть class a b class b, наследуемый откласса а. я хочу неполно объявить класс b перед классом а. как это синтаксически сделать? нигде не могу найти инфу об этом.
Все ссылки на статьи и ролики моего канала Old Programmer :
Программирование. Тематическое оглавление моего Zen-канала (Old Programmer) . Все ссылки на материалы об объектно-ориентированном программировании собраны в один раздел .
Сегодня продолжим тему объектно-ориентированного программирования ( ООП ), а она, ну право, необъятна. И это мы еще не дошли до Python. Но дойдем, я вам обещаю, дойдем. Сегодня разберем два специальных метода: конструктор (constructor) и деструктор (destructor). Эти методы не вызываются программно, они запускаются автоматически. Конструктор - при создании объекта, деструктор - при уничтожении объекта.
Объектно-ориентированное программирование (C++). Статья 1 (динамические и статические объекты, три кита ООП)
Конструкторы и деструкторы в ООП
Зачем нам эти методы? - спросите вы. А вот зачем. В конструктор можно поместить код, который инициализирует некоторые переменные класса, выделяет им место в памяти (если нужно), присваивает определенные значения. Типичным примером может быть объект, который описывает окно. Нужно задать размер окна, цвет, заголовок, другие его свойства. Задать элементы в окне. Метод деструктор служит для того, чтобы, например, освободить память, если она выделялась в процессе выполнения конструктора или других методов, ну и, возможно, произвести другие изменения, которые важно сделать именно после уничтожения объекта. Конструктор и деструктор делают объект самодостаточной единицей программирования.
В классе могут быть несколько конструкторов, отличающихся друг от друга порядком и типом параметров. В соответствии с известным принципом перегрузки функций. Я вам не рассказывал о перегрузке? Но вот видите, сколько еще у нас с вами открытий впереди чудных. Да и самое важное. Имя конструктора совпадает с именем класса. Деструктор также имеет имя, совпадающее с именем класса но со знаком тильда ~ впереди. Параметры для конструктора передаются при создании объекта (см. программу ниже).
Пример использования конструктора и деструктора в языке C++
Программа ниже как раз и демонстрирует работу конструктора A(int, int) и деструктора ~A() . Мы видим, что при создании объекта динамически создаются две переменные типа int и им присваиваются значения, переданные через параметры конструктора. При уничтожении объекта ( delete ) автоматически выполняется деструктор, где освобождается память, которая была выделена в конструкторе. В примере ниже у объекта есть также метод mul , который возвращает произведение двух переменных класса, значения которых задаются при создании объекта.
Вот, на сегодня и все, я надеюсь вы продвигаетесь в своем стремлении стать программистами. Читайте мои статьи на канале Old Programmer и ставьте лайки. Подписывайтесь на мой канал. Пока!
Возможно вы заметили, что определяя класс, мы не можем инициализировать его поля (члены) в самом определении. Можно присвоить им значение, написав соответствующий метод класса и вызвав его, после создания объекта вне класса. Такой способ не совсем удобен, так как объявляя, допустим, 33 объекта класса нам придется 33 раза вызывать метод, который присваивает значения полям класса. Поэтому, как правило, для инициализации полей класса, а так же для выделения динамической памяти, используется конструктор.
Конструктор (от construct – создавать) – это особый метод класса, который выполняется автоматически в момент создания объекта класса. То есть, если мы пропишем в нем, какими значениями надо инициализировать поля во время объявления объекта класса, он сработает без “особого приглашения”. Его не надо специально вызывать, как обычный метод класса.
В строках 11 – 17 определяем конструктор: имя должно быть идентично имени класса; конструктор НЕ имеет типа возвращаемого значения (void в том числе). Один объект объявляется сразу во время определения класса – строка 25. При запуске программы, конструктор этого объекта сработает даже до входа в главную функцию. Это видно на следующем снимке:
программа еще не дошла до выполнения строки 29 setlocale ( LC_ALL , "rus" ) ; , а конструктор уже “отчитался”, что сработал (кириллица отобразилась некорректно). В строке 30 – смотрим, что содержат поля класса. Второй раз конструктор сработает в строке 32, во время создания объекта obj2 .
Деструктор (от destruct – разрушать) – так же особый метод класса, который срабатывает во время уничтожения объектов класса. Чаще всего его роль заключается в том, чтобы освободить динамическую память, которую выделял конструктор для объекта. Имя его, как и у конструктора, должно соответствовать имени класса. Только перед именем надо добавить символ ~
Добавим деструктор в предыдущий код. И создадим в классе два конструктора: один будет принимать параметры, второй – нет.
Читайте также: