Сколько параметров можно передать в деструктор

Урок №120. Деструкторы

На этом уроке мы рассмотрим, что такое деструкторы в языке С++, зачем они нужны, как их использовать и нюансы, которые могут возникнуть при их использовании.

Деструкторы

Деструктор — это специальный тип метода класса, который выполняется при удалении объекта класса. В то время как конструкторы предназначены для инициализации класса, деструкторы предназначены для очистки памяти после него.

Когда объект автоматически выходит из области видимости или динамически выделенный объект явно удаляется с помощью ключевого слова delete, вызывается деструктор класса (если он существует) для выполнения необходимой очистки до того, как объект будет удален из памяти. Для простых классов (тех, которые только инициализируют значения обычных переменных-членов) деструктор не нужен, так как C++ автоматически выполнит очистку самостоятельно.

Однако, если объект класса содержит любые ресурсы (например, динамически выделенную память или файл/базу данных), или, если вам необходимо выполнить какие-либо действия до того, как объект будет уничтожен, деструктор является идеальным решением, поскольку он производит последние действия с объектом перед его окончательным уничтожением.

Имена деструкторов

Так же, как и конструкторы, деструкторы имеют свои правила, которые касаются их имен:

деструктор должен иметь то же имя, что и класс, со знаком тильда (

деструктор не может принимать аргументы;

деструктор не имеет типа возврата.

Из второго правила вытекает еще одно правило: для каждого класса может существовать только один деструктор, так как нет возможности перегрузить деструкторы, как функции, и отличаться друг от друга аргументами они не могут.

Пример использования деструктора на практике

Рассмотрим простой класс с деструктором:

Massiv() вызывается тоже здесь

Результат выполнения программы:

The value of element 7 is 8

В первой строке функции main() мы создаем новый объект класса Massiv с именем arr и передаем длину ( length ) 15. Это приводит к вызову конструктора, который динамически выделяет память для массива класса ( m_array ). Мы должны здесь использовать динамическое выделение, поскольку на момент компиляции мы не знаем длину массива (это значение нам передает caller).

В конце функции main() объект arr выходит из области видимости. Это приводит к вызову деструктора

Massiv() и к удалению массива, который мы выделили ранее в конструкторе!

Выполнение конструкторов и деструкторов

Как мы уже знаем, конструктор вызывается при создании объекта, а деструктор — при его уничтожении. В следующем примере мы будем использовать стейтменты с cout внутри конструктора и деструктора для отображения их времени выполнения:

Результат выполнения программы:

Constructing Another 1
1
Constructing Another 2
2
Destructing Another 2
Destructing Another 1

Идиома программирования RAII

Идиома RAII (англ. «Resource Acquisition Is Initialization» = «Получение ресурсов есть инициализация») — это идиома объектно-ориентированного программирования, при которой использование ресурсов привязывается к времени жизни объектов с автоматической продолжительностью жизни. В языке C++ идиома RAII реализуется через классы с конструкторами и деструкторами. Ресурс (например, память, файл или база данных) обычно приобретается в конструкторе объекта (хотя этот ресурс может быть получен и после создания объекта, если в этом есть смысл). Затем этот ресурс можно использовать, пока объект жив. Ресурс освобождается в деструкторе при уничтожении объекта. Основным преимуществом RAII является то, что это помогает предотвратить утечку ресурсов (например, памяти, которая не была освобождена), так как все объекты, содержащие ресурсы, автоматически очищаются.

В рамках идиомы программирования RAII объекты, располагающие ресурсами, не должны быть динамически выделенными, так как деструкторы вызываются только при уничтожении объектов. Для объектов, выделенных из стека, это происходит автоматически, когда объект выходит из области видимости, поэтому нет необходимости беспокоиться о том, что ресурс в конечном итоге не будет очищен. Однако за очистку динамически выделенных объектов, которые выделяются из кучи, уже пользователь несет ответственность: если он забыл её выполнить, деструктор вызываться не будет, и память как для объекта класса, так и для управляемого ресурса будет потеряна — произойдет утечка памяти!

Класс Massiv из программы, приведенной в начале этого урока, является примером класса, который реализует принципы RAII: выделение в конструкторе, освобождение в деструкторе. std::string и std::vector — это примеры классов из Стандартной библиотеки С++, которые следуют принципам RAII: динамическая память выделяется при инициализации и автоматически освобождается при уничтожении.

Правило: Используйте идиому программирования RAII и не выделяйте объекты вашего класса динамически.

Предупреждение о функции exit()

Если вы используете функцию exit(), то ваша программа завершится, и никакие деструкторы не будут вызваны. Будьте осторожны, если в таком случае вы полагаетесь на свои деструкторы для выполнения необходимой работы по очистке (например, перед тем, как выйти, вы записываете что-нибудь в лог-файл или в базу данных).

Заключение

Используя конструкторы и деструкторы, ваши классы могут выполнять инициализацию и очистку после себя автоматически без вашего участия! Это уменьшает вероятность возникновения ошибок и упрощает процесс использования классов.

Поделиться в социальных сетях:

Урок №119. Делегирующие конструкторы

Комментариев: 13

Не совсем понял фразу в тексте: «В рамках идиомы программирования RAII объекты, располагающие ресурсами, не должны быть динамически выделенными, так как деструкторы вызываются только при уничтожении объектов». Ведь деструкторы нужны в основном только при использовании динамической памяти, а тут написано, что «объекты не должны быть динамически выделенными». Как-то я запутался. Даже GOOGLE не помог.
Кто бы мне объяснил, я бы был очень благодарен.

Коротко, емко, толково. Спасибо. Хотя все это и знал, но не могу не оставить положительный отзыв за грамотное и сжатое изложение.

Источник

BestProg

Деструкторы. Определение деструктора. Общедоступные и приватные деструкторы. Примеры использования деструкторов. Отличия между конструкторами и деструкторами

Содержание

Поиск на других ресурсах:

1. Какое назначение деструктора в классе?

Деструктор – это обратная по отношению к конструктору функция.

Имя деструктора совпадает с именем класса, перед которым следует символ ‘

2. Пример использования общедоступного деструктора

Общий код модуля, в котором объявляется структура и класс.

Демонстрация использования данного класса в другом методе.

Да, можно. Такой деструктор называется приватным деструктором.

4. В каких случаях целесообразно объявлять приватные деструкторы?

Использование приватных деструкторов целесообразно в тех случаях, когда обычным пользователям запрещается освобождать память для прежде созданных объектов (уничтожать раньше созданные объекты).

5. Какие ограничения возникают при работе с объектами класса, если в классе объявлен приватный деструктор?

Если в классе объявлен приватный деструктор, то возникают следующие ограничения:

Это связано с тем, что такие объекты в дальнейшем невозможно будет уничтожить.

Например. Пусть задан класс, в котором объявлен приватный деструктор.

Если попробовать создать объект класса в другом программном коде

то компилятор выдаст ошибку

6. Может ли деструктор иметь параметры?

Деструктор не может иметь параметров.

7. Какие основные отличия между использованием конструкторов и деструкторов в классах?

При использовании в классе, между конструктором и деструктором можно определить следующие основные отличия:

Источник

Сколько параметров можно передать в деструктор

Любой переменной, участвующей в работе программы, требуется память и некоторое начальное значение. Для переменных встроенных типов размещение в памяти обеспечивается компилятором. Для локальных переменных память выделяется из стека программы и занимается для хранения значения данной переменной до тех, пока не закончится время ее жизни. Сложные типы данных также должны размещаться в памяти и уничтожаться когда их время жизни закончилось. Это осуществляется с использованием конструкторов и деструкторов.

имя класса, предназначенная для уничтожения переменных (delete).

Одной из особенностей конструктора и деструктора является то, что в отличие от всех остальных функций, у них нет возвращаемого значения.

4.3.1. Конструкторы

Конструктор по умолчанию

Конструктор, не требующий аргументов, называется конструктором по умолчанию. Конструктор по умолчанию не имеет аргументов и инициализирует все переменные члены какими-либо начальными значениями.

При создании любого экземпляра класса вызывается конструктор. Если при описании экземпляра не указываются никакие параметры – вызывается конструктор по умолчанию:

Полный конструктор

Полный конструктор позволяет явно инициализировать все переменные-члены класса.

Если при описании экземпляра класса в скобках указать параметры, при создании экземпляра класса будет вызван полный конструктор и переменные-члены инициализируются указанными значениями.

Неполный конструктор

Возможен и неполный конструктор, когда в списке параметров указываются не все возможные параметры для инициализации членов класса, а только наиболее часто используемые. Остальные параметры будут инициализированы значениями по умолчанию.

Инициализация переменных-членов класса в конструкторах может осуществляться не только в теле конструктора, но и после оператора :. При этом, во время присваивания переменной-члену значения, будет вызываться не оператор присваивания, а конструктор. Для встроенных типов данных, таких как double или int, это не существенно, но если членами класса являются абстрактные типы, вызов конструктора вместо оператора присваивания будет выполняться быстрее.

Конструктор копирования

Конструктор копирования создает копию уже существующего экземпляра класса, копируя поэлементно переменные-члены. Конструктор копирования также используется при передаче экземпляров класса в функции по значению. Обратите внимание, что экземпляр класса передается в конструктор по константной ссылке.

4.3.2. Деструктор (пример 4.4. Конструктор и деструктор класса Матрица)

Деструктор осуществляет освобождение памяти, например уничтожение объектов размещенных динамически.

В классе Lens никакого динамического размещения не происходило, поэтому деструктор будет пустой, но его наличие все равно обязательно. Для примера реализации деструктора, представим, что имеется класс Matrix, который в конструкторе динамически создает двумерный массив размерности n x m. Тогда деструктор должен освобождать память, которую выделяет конструктор.

Конструктор вызывается в момент создания переменной, деструктор вызывается когда время жизни переменной закончилось, то есть когда встречается закрывающая фигурная скобка > блока, в которой была объявлен экземпляр класса, либо когда вызывается оператор delete при динамическом размещении экземпляра класса.

4.3.3. Проверка правильности параметров. Исключительные ситуации

Конструкторы должны проверять передаваемые им аргументы на корректность значений. Например, показатель преломления не может быть меньше 1. Что делать, если в конструктор были переданы неправильные параметры? Для этого в языке С++ существуют исключительные ситуации.

Класс exception является стандартным базовым классом C++ для всех исключений. Исключения можно сгенерировать в случае возникновения непредвиденной ошибки, например мы предполагаем что при вызове класса Lens никто не будет пытаться задать показатель преломления меньше 1, но при этом такая ситуация возможна, и это может привести к ошибке. Сгенерировать исключительную ситуацию можно при помощи оператора throw:

Для обработки возникшей исключительной ситуации используются try и catch блоки.

В блок try заключается код, в котором предположительно могут возникнуть исключительные ситуации. В нашем случае это вызов конструктора. Кроме того, в этот же блок заключают операторы, которые должны быть пропущены в случае, если исключение возникает. В нашем случае вычисление и вывод на экран параксиальных характеристик не имеет смысл выполнять, если в конструкторе возникла ошибка.

Если при выполнение какого-то оператора из блока try возникает исключение – управление сразу переходит к блоку catch. В блоке catch в скобках указывается тип исключения (exception это наиболее общий вид исключения, возможны и другие типы) и имя исключения. Внутри блока catch необходимо обработать ошибку. В нашем случае мы просто выводим на экран сообщение, в каких-то случаях потребуется более сложная обработка. Функция what() содержит текст, сгенерированный в момент создания исключения.

В результаты выполнения данного блока программы на экран выведется сообщение » Index of refraction should be greater than 1.».

Если никаких исключений в try-блоке не происходит, программа игнорирует его catch-обработчик.

Если исключение было сгенерировано, но перехватывание исключения в блоке try не происходило, функция, содержащая этот оператор, немедленно завершается, и программа пытается найти охватывающий try-блок в вызывающей функции. Если нигде в вызывающих функциях не найдется блок try, программа прервется с сообщением об ошибке.

Источник

Есть ли деструктор в C#?

Чем отличается деструктор от финализатора

Деструктор — это метод для деинициализации объекта. Здесь важно упомянуть о такой штуке, как deterministic destruction. То есть мы точно знаем, когда объект будет удален. Чаще всего это происходит, когда заканчивается область видимости объекта, или программист явно освобождает память(в с/с++).

А вот определение финализатора из Википедии.

Финализатор — это метод класса, который автоматически вызывается средой исполнения в промежутке времени между моментом, когда объект этого класса опознаётся сборщиком мусора как неиспользуемый, и моментом удаления объекта (освобождения занимаемой им памяти). Это уже обратная штука — nondeterministic destruction.
То есть главный минус финализатора в том, что мы не знаем, когда он вызовется. Это может создать огромное количество проблем.

В Visual C# есть финализатор, который создается с помощью синтаксиса создания деструктора в С++, и который некоторыми даже называется как деструктор, хотя таковым не является. Выполнен он через метод Finalize, который нельзя переопределить(в C# нет, но в VB можно), поэтому и приходится использовать синтаксис деструктора через тильду(

ClassName). И только при компиляции в IL, компилятор называет его Finalize. При выполнении этот метод также вызывает финализатор родительского класса.

Если заглянуть в спецификацию языка программирования C#(4.0 на данный момент), то там слово «finalizer» ни разу не встречается. Ну это еще можно объяснить. Финализатор тесно связан со сборщиком мусора, который в свою очередь является частью среды выполнения(CLR в нашем случае), но не самого языка программирования.

Теперь пойдем еще дальше и залезем в спецификацию CLI(ECMA-335). Здесь вот что написано.

A class definition that creates an object type can supply an instance method (called a finalizer) to be called
when an instance of the class is no longer reachable.

Это, несомненно, описание финализатора, хотя на мой взгляд, немного неточное.

Далее, идем на msdn. Ни в одной статье не встречается слово finalizer в чистом виде — зато почти всегда используется слово деструктор. Возникает закономерный вопрос — почему люди называют деструктором то, что им не является. Получается, что майкрософтовские разработчики сознательно поменяли значение этого слова. И вот почему.

Мы знаем, что в Visual C# nondeterministic destruction. Это значит, что даже если область видимости объекта закончилась, и сборщик мусора понял, что можно освобождать занимаемую им память, не факт, что это произойдет незамедлительно. То есть это чистой воды финализатор. Так как он использует синтаксис, который во всех языках используется для деструктора, можно предположить, что в Visual C# нет способа определить деструктор(в общем понимании). Это значит, что его просто-напросто нет. Да, необходимости в нем тоже особой нет, но нужно согласиться с тем, что и самого деструктора в Visual C# быть не может.

Вывод — либо я идиот, и совсем все неправильно понял(а вероятность этого довольно высока), либо нужно смириться с этим, и называть деструктором то, что внешне на него похоже(тильда, привет), но по сути является финализатором. Надо же как-то жить в этом мире.

Источник

Виртуальные функции и деструктор

Когда-то давным давно я собирался и даже обещал написать про механизм виртуальных функций относительно деструкторов. Теперь у меня наконец появилось свободное время и я решил воплотить эту затею в жизнь. На самом деле эта мини-статья служит «прологом» к моей следующей статье. Но я постарался изложить доходчиво и понятно основные моменты по текущей теме. Если вы чувствуете, что еще недостаточно разобрались в механизме виртуальных вызовов, то, возможно, вам следует для начала прочитать мою предыдущую статью.

Сразу же, как обычно, оговорюсь, что: 1) статья моя не претендует на полноту изложения материала; 2) мегапрограммеры ничего нового здесь не узнают; 3) материал не новый и давно описан во многих книгах, но если явно об этом не прочитать и самому специально не задумываться, то можно о некоторых моментах даже не подозревать (до поры, до времени). Также прошу прощения за надуманные примеры 🙂

Виртуальные деструкторы

Если вы уже знаете и умеете использовать виртуальные функции, то просто обязаны знать, когда и зачем нужны виртуальные деструкторы. Иначе нижеследующий текст был написан именно для вас.

Основное правило: если у вас в классе присутствует хотя бы одна виртуальная функция, деструктор также следует сделать виртуальным. При этом не следует забывать, что деструктор по умолчанию виртуальным не будует, поэтому следует объявить его явно. Если этого не сделать, у вас в программе почти наверняка будут утечки памяти (memory leaks). Чтобы понять почему, опять же много ума не надо. Рассмотрим несколько примеров.

В первом случае создадим объект производного класса в стеке:

using std :: cout ;
using std :: endl ;

Всем ясно, что вывод программы будет следующим:

потому что сначала конструируется базовая часть класса, затем производная, а при разрушении наоборот — сначала вызывается деструктор производного класса, который по окончании своей работы вызывает по цепочке деструктор базового. Это правильно и так должно быть.

Попробуем теперь создать тот же объект в динамической памяти, используя при этом указатель на объект базового класса (код классов не изменился, поэтому привожу только код функции main()):

int main ( )
<
A * pA = new B ;
delete pA ;
return EXIT_SUCCESS ;
>

На сей раз конструируется объект так, как и надо, а при разрушении происходит утечка памяти, потому как деструктор производного класса не вызывается:

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

Чтобы этого избежать, деструктор в базовом классе должен быть объявлен как виртуальный:

using std :: cout ;
using std :: endl ;

int main ( )
<
A * pA = new B ;
delete pA ;
return EXIT_SUCCESS ;
>

Теперь-то мы получим желаемый порядок вызовов:

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

Виртуальные функции в деструкторах

Давайте для начала рассмотрим ситуацию с вызовом виртуальных функций внутри класса. Предположим, что у нас есть Кот, который просит покушать мяуканьем, а затем приступает к процессу 🙂 Так поступают многие коты, но не Чеширский! Чеширский, как известно, мало того что вечно улыбается, так еще и довольно разговорчив, поэтому мы научим его говорить, переопределив метод speak():

using std :: cout ;
using std :: endl ;

class Cat
<
public :
void askForFood ( ) const
<
speak ( ) ;
eat ( ) ;
>
virtual void speak ( ) const < cout "Meow! " ; >
virtual void eat ( ) const < cout "*champing*" endl ; >
> ;

class CheshireCat : public Cat
<
public :
virtual void speak ( ) const < cout "WTF?! Where \' s my milk? =) " ; >
> ;

delete cats [ 0 ] ; delete cats [ 1 ] ;
return EXIT_SUCCESS ;
>

Вывод этой программы будет следующим:

Ordinary Cat: Meow! *champing*
Cheshire Cat: WTF?! Where’s my milk? =) *champing*

Рассмотрим код более подробно. Есть класс Cat с парой виртуальных методов, один из которых переопределен в производном CheshireCat. Но всё самое интересное происходит в методе askForFood() класса Cat.

Как видно, метод всего лишь содержит вызовы двух других методов, однако конструкция speak() в данном контексте эквивалента this->speak(), то есть вызов происходит через указатель, а значит — будет использовано позднее связывание. Вот почему при вызове метода askForFood() через указатель на CheshireCat мы видим то, что и хотели: механизм виртуальных функций работает исправно даже несмотря на то, что вызов непосредственно виртуального метода происходит внутри другого метода класса.

А теперь самое интересное: что будет, если попытаться воспользоваться этим в деструкторе? Модернизируем код так, чтобы при деструкции наши питомцы прощались, кто как умеет:

using std :: cout ;
using std :: endl ;

class Cat
<
public :
virtual

Cat ( ) < sayGoodbye ( ) ; >
void askForFood ( ) const
<
speak ( ) ;
eat ( ) ;
>
virtual void speak ( ) const < cout "Meow! " ; >
virtual void eat ( ) const < cout "*champing*" endl ; >
virtual void sayGoodbye ( ) const < cout "Meow-meow!" endl ; >
> ;

class CheshireCat : public Cat
<
public :
virtual void speak ( ) const < cout "WTF?! Where \' s my milk? =) " ; >
virtual void sayGoodbye ( ) const < cout "Bye-bye! (:" endl ; >
> ;

delete cats [ 0 ] ; delete cats [ 1 ] ;
return EXIT_SUCCESS ;
>

Можно ожидать, что, как и в случае с вызовом метода speak(), будет выполнено позднее связывание, однако это не так:

Ordinary Cat: Meow! *champing*
Cheshire Cat: WTF?! Where’s my milk? =) *champing*
Meow-meow!
Meow-meow!

Почему? Да потому что при вызове виртуальных методов из деструктора компилятор использует не позднее, а раннее связывание. Если подумать, зачем он делает именно так, всё становится очевидным: нужно просто рассмотреть порядок конструирования и разрушения объектов. Все помнят, что конструирование объекта происходит, начиная с базового класса, а разрушение идет в строго обратном порядке. Таким образом, когда мы создаем объект типа CheshireCat, порядок вызовов конструкторов/деструкторов будет таким:

Если же мы захотим внутри деструктора

Cat() совершить виртуальный вызов метода sayGoodbye(), то фактически попытаемся обратиться к той части объекта, которая уже была разрушена.

Мораль: если в вашей голове витают помыслы выделить какой-то алгоритм «зачистки» в отдельный метод, переопределяемый в производных классах, а затем виртуально вызывать его в деструкторе, у вас ничего не выйдет.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *