Статические и динамические конструкторы

Обновлено: 27.04.2024

Я, как и любой другой автор, всегда могу упустить интересный момент обсуждаемой темы (что подтвердилось на практике). А потому задаваемый вопрос может закрывать пробел в статье. Ответ на конкретный вопрос, как правило, дать несложно. Сложнее его аккуратно сформулировать так, чтобы ответ являлся законченной частью статьи. Поэтому, как правило, на первых порах я ограничиваюсь конкретным ответом на конкретный вопрос, а в статью временно вставляю ссылку на пост, где был дан ответ. А когда дойдут руки, то вместо ссылки пишу нормальное пояснение. Технические возможности блога не позволяют в комментариях пользоваться широкими возможностями, доступными на форуме (то как выделение текста жирным, вставка фрагментов исходников в удобном для чтения виде и т.п.), поэтому будет удобнее, если вопрос и ответ будут опубликованы на форуме

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

Исторически сложилось, что раньше (когда ещё не было блога) статьи располагались на форуме и представлены были в виде двух тем. Первая тема создавалась в специально отведённой свалке и представляла собой черновик, который со временем дорабатывался до законченной статьи. После этого статья переезжала во вторую тему в тематическом разделе. А первая тема оставалась дополнительной свалкой для замечаний и мелких вопросов по теме. Ссылку на старое местоположение данной свалки я помещаю в начале статьи. Вопросы, по возможности, прошу создавать в отдельных темах, но если вопрос действительно мелкий, то можно его задать и в указанной свалке.

  • 1. Предисловие
  • 2. Терминология и классификация
  • 3. Время жизни объекта и память, выделяемая под объект
  • 4. Конструкторы и деструкторы
    4.1. Автоматический объект
    4.2. Статический объект
    4.3. Динамический объект
  • 5. Неочевидные моменты при работе с конструкторами и деструкторами в Си++
    5.1. Конструкторы и деструкторы не являются процедурами
    5.2. Вызов деструкторов при исполнении return посередине процедуры
    5.3. Вызов деструкторов при исключительных ситуациях
    5.4. Вызов деструкторов при принудительном завершении потока
    5.5. Классы, содержащие указатели
    5.6. Отличия между new/delete и malloc/free
    5.7. Отличия между конструированием объекта и присваиванием
    5.8. Некоторые тонкости при наличии печати в стандартный вывод внутри деструктора
    5.9. Исключительные ситуации в процессе работы конструкторов и деструкторов
  • 6. Заключение
  • 7. Ссылки на темы, где обсуждался данный вопрос
  • 8. Внешние ссылки по данной тематике


1. Предисловие

Прежде всего хочется отметить, что я не собираюсь рассказывать о том, что такое конструкторы и деструкторы. Об этом можно почитать в любой книге по Си++. Создание любого объекта в программе состоит из двух этапов: выделение аппаратного ресурса под объект (память или регистр) и инициализация объекта (запись значения). С точки зрения языка оба этапа зачастую выражаются в виде одного оператора языка, а потому для начинающих эти два этапа явно не разделяются. Авторы многих книг так же не уделяют внимания этому факту, поэтому у многих начинающих возникает множество проблем с созданием и удалением объектов. Но чтобы пояснения стали понятны, придётся уделить некоторое время занудному процессу объяснения терминологии и классификации. По крайней мере на текущий момент я не совсем понимаю, как можно изложить материал без этого, чтобы не запутать людей ещё больше

Занудство сосредоточено в разделах 2 и 3. По большому счёту эти разделы читать необязательно - только посмотреть определения в конце этих разделов. В разделе 4 всё пояснено на примерах-аналогиях, по идее этого материала для понимания достаточно. Однако разделы 2 и 3 могут оказаться полезными для того, чтобы привести собственные знания в порядок и разложить по полочкам

Изначально я планировал ограничить статью лишь разделением выделения памяти и инициализации объекта. Однако в процессе написания вспомнились некоторые тонкие моменты по работе с конструкторами и деструкторами в Си++. Моменты вспомнились и тем, кто читал статью. Все эти тонкости выходят за изначально предполагаемые рамки данной статьи, но тем не менее решил написать о них и собрал их в разделе 5 - не пропадать же добру


2. Терминология и классификация

Большинство программистов привыкло к той терминологии, на которой построено большинство книг и учебников. Это так называемая "бытовая" терминология. Однако разработчики компиляторов пользуются немного другой терминологией. И мне хотелось бы в описании воспользоваться именно этой терминологией, т.к. она более точно описывает понятия. Ну и если кто будет читать техническую литературу на околокомпиляторные темы, он скорее всего увидит ту же самую терминологию. Поэтому на всякий случай для всех терминов напишу варианты на английском языке

Вместо слова "переменная" (variable) будем пользоваться словом "объект" (object). Объект - это сущность, которая описывает аппаратный ресурс - регистр или память. Внимание! Не путать с понятием "объект Си++ как экземпляр класса". Поскольку для пояснений нет принципиальной разницы между тем, будет объект распределён на регистр или в памяти, в дальнейшем я не буду использовать фразу "аппаратный ресурс", а буду просто говорить "память". Итак, объект - это сущность, которая описывает некоторый участок памяти в момент исполнения программы. Память может быть выделена динамически в процессе работы программы, а потому эту память описывать словом "переменная" было бы некорректным. Именно поэтому мы будем пользоваться термином "объект". Другими словами, объект - это переменная языка или динамически выделенный экземпляр (класса).

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

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

Переменная, описаная внутри функции (например, внутри функции func1), называется "локальная" (local variable). Когда говорят о локальности, то обычно в первую очередь подразумевают локальную область видимости. Т.е. к этой переменной по имени можно обращаться только из той функции или лексического блока, в котором переменная определена. Однако к значению переменной мы можем обратиться из другой функции. Например, если мы в функции func1 возьмём адрес на переменную и передадим его в другую функцию (например, func2), то из функции func2 мы сможем через указатель обратиться к значению переменной (несмотря на то, что обратиться к переменной по имени не можем). Именно поэтому компиляторщики вместо термина "локальная" используют термин "автоматическая". И, таким образом, то, что обычно называют "локальная переменная", мы будем называть "автоматический объект" (automatic object) - от понятия "автоматическое время жизни".

Переменная, описанная вне функции, называется "глобальная" (global variable). Здесь так же в первую очередь подразумевается глобальная область видимости. Т.е. к глобальной переменной можно обращаться по имени из всех функций (или из всех функций данного файла, если у переменной есть модификатор static). Для компиляторщиков с точки зрения обсуждаемого в теме вопроса нет разницы между глобальными переменными, которые описаны с модификатором static или без него. Поэтому всё то, что обычно называют "глобальными переменными" мы будем называть термином "статический объект" (static object) - от понятия "статическое (т.е. не автоматическое) время жизни".

Внимание! Слово "статический" никак не связано с модификатором static. Уходя немного в сторону, разработчики языка Си немного погорячились, когда придумали слово "static", ибо исторически (с прошлых языков программирования) словом "static" называли именно глобальные переменные. А то, что принято называть "глобальная переменная" и "глобальная переменная с модификатором static", у компиляторщиков (да и в технической литературе по компиляторам) называют словами "global" и "local" соответственно. Но данный аспект нам сейчас не важен. На важно лишь не перепутать понятия "статический объект" и "модификатор static"

Переменная языка Си\Си++, определённая внутри функции и описанная с модификатором static, с точки зрения времени жизни попадает под понятие "статический объект"

Всё то, что создано динамически (а поскольку мы говорим о конструкторах, то прежде всего всё то, что выделено оператором new), мы будем называть "динамический объект" (dynamic object).

В языке Pascal есть понятие "функции" (function) и "процедуры" (procedure) - они отличаются тем, что одно возвращает значение, а другое - нет. В языке Си порешили, что всё будет называться "функциями". С появлением Си++ пришло понятие "метод" (method). С точки зрения компилятора на низком уровне - все эти понятия являются просто набором кодов, а потому компиляторщики все эти понятия объединяют в одно и называют "процедура".

  • Объект (object) - сущность, описывающая некоторый участок памяти. Другими словами, это переменная языка или динамически выделенная память (например, оператором new). Не путать с "объектом Си++". Наше понятии "объект" не завязано ни на какой конкретный язык программирования, несмотря на то, что пояснения ведётся в контексте языка Си++
  • Автоматический объект (automatic object) - локальная переменная функции или лексического блока
  • Статический объект (static object) - глобальная переменная (независимо от наличия модификатора static) или локальная переменная, описанная с модификатором static
  • Динамический объект (dynamic object) - динамически выделенная память (например, оператором new)
  • Процедура (procedure) - процедура, функция, метод


3. Время жизни объекта и память, выделяемая под объект

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

Временем жизни (lifetime) объекта называют тот период времени работы программы, в течение которого память, выделенная при создании объекта, закрепляется за этим объектом. Другими словами, если мы в течение времени жизни объекта будем обращаться к его памяти, то мы прочитаем корректное значение (при условии, что у нас в программе нет ошибок по записи в чужую память)

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

FIXME для особо продвинутых и любознательных надо бы пояснить разницу между техническим выделением памяти под объект и логическим рождением объекта. Пока только ссылки, но момент действительно интересный:
[C++] Взятие адреса конструктора. Физическое время существование объекта.
[C++] Взятие адреса конструктора. Физическое время существование объекта.
[C++] Взятие адреса конструктора. Физическое время существование объекта.

Время жизни статического объекта условно можно считать равным времени жизни программного модуля, в котором находится объект. Программным модулем называют любую логически готовую к исполнению единицу, оформленную в виде отдельного файла. Другими словами, программный модуль - это исполняемый файл (для тех, кто под windows - это файл *.exe) или динамическая библиотека (для тех, кто под windows - это файл *.dll). Таким образом, если статический объект попал в исполняемый файл, то его временем жизни можно считать "вся программа": память под объект выделяется до того, как началось исполнение main; начиная с момента, когда мы вошли в main и заканчивая моментом, когда мы вышли из main (или вызвали exit или любой другой способ завершения программы), эта память закреплена за объектом. После того, как мы вышли из main (вызвали exit и т.п.) условно можно считать, что память освобождается. Аналогично случаю с автоматическими объектами, реального освобождения памяти не происходит, но нам это не важно, поскольку мы попросили программу завершиться, а значит, больше никаких действий выполнять не будем. А если статический объект попал в динамическую библиотеку, то время жизни объекта начинается в тот момент, когда динамическая библиотека загружена в память, и заканчивается в тот момент, когда динамическая библиотека выгружена из памяти. Некоторые забывают про эту особенность.

Время жизни динамического объекта начинается в момент явного (т.е. написанного программистом) создания динамической памяти (по сути дела в момент вызова new). Время жизни динамического объекта заканчивается в момент явного удаления динамической памяти (т.е. по сути вызов delete). Если программа написана таким образом, что delete не вызывается, то можно считать, что память от объекта не освобождается. Реально эта память освобождается, но уже на уровне процессов операционной системы, но вплоть до последней исполненной команды нашей программы эта память принадлежит динамическому объекту.

  • Автоматический объект. Время жизни начинается в момент входа в процедуру или лексический блок, в котором объявлен объект и заканчивается при выходе из процедуры или лексического блока.
  • Статический объект, попавший в исполняемый файл. Время жизни начинается до входа в процедуру main и заканчивается после выхода из процедуры main
  • Статический объект, попавший в динамическую библиотеку. Время жизни начинается с момента загрузки динамической библиотеки и заканчивается в момент выгрузки динамической библиотеки.
  • Динамический объект. Время жизни начинается в момент явного выделения памяти для объекта и заканчивается в момент явного освобождения памяти. Если память явным образом не освобождается, то данная память так и остаётся использованной и будет высвобождена средствами операционной системы


4. Конструкторы и деструкторы

Прежде всего хочется указать на одну из самых распространённых ошибок начинающих: они считают, что конструктор выделяет память для объекта (создаёт объект), а деструктор - освобождает память (удаляет объект). Это неверно! Процесс выделения памяти для объекта описан в предыдущей главе. А конструктор только инициализирует объект (т.е. задаёт объекту некоторое начальное значение). Очень важно это понимать. Непонимание обычно растёт из того, что эти два этапа с точки зрения языка обычно выглядят в виде одного оператора. Компилятор обеспечивает механизм, при котором в процессе создания объекта выделяется память под объект и вызывается метод-конструктор. Но эти две вещи не обязательно выполняются подряд одна за другой. Аналогично в процессе удаления объект вызывается метод-деструктор и затем происходит высвобождение памяти.

Чтобы лучше понять, что на самом деле творится в программе, попробую пояснить на более низкоуровневых аналогах, описанных на языке Си. Все разъяснения оформлены в виде комментариев к соответствующим участкам программ.

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

В следующем примере класс с именем Taxi определяется с помощью простого конструктора. Затем оператор new создает экземпляр этого класса. Конструктор Taxi вызывается оператором new сразу после того, как новому объекту будет выделена память.

Конструктор, который не принимает никаких параметров, называется конструктором без параметров. Конструкторы без параметров вызываются всякий раз, когда создается экземпляр объекта с помощью оператора new , а аргументы в new не передаются. Дополнительные сведения см. в разделе Конструкторы экземпляров.

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

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

Конструкторы для типов структур похожи на конструкторы классов, но structs не могут содержать явный конструктор без параметров, так как он предоставляется компилятором автоматически. Этот конструктор инициализирует каждое поле в struct со значением по умолчанию. При этом конструктор без параметров вызывается только в том случае, если экземпляр struct создается с помощью переменной new . Например, этот код использует конструктор без параметров, Int32чтобы гарантировать, что целое число инициализируется:

Однако следующий код вызывает ошибку компилятора, так как он не используется new , и потому что он пытается использовать объект, который не был инициализирован:

Кроме того, объекты на основе structs (включая все встроенные числовые типы) можно инициализировать или назначить, а затем использовать, как в следующем примере:

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

Оба класса и structs могут определять конструкторы, принимающие параметры. Конструкторы, принимающие параметры, необходимо вызывать с помощью оператора new или base. Классы и structs могут определять также несколько конструкторов; для определения конструктора без параметров ни один их них не требуется. Пример:

Этот класс можно создать, воспользовавшись одним из следующих операторов:

Конструктор может использовать ключевое слово base для вызова конструктора базового класса. Пример:

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

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

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

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

Применение ключевого слова this в приведенном выше примере привело к вызову конструктора:

Конструкторы могут иметь пометку public, private, protected, internal, protected internal или private protected. Эти модификаторы доступа определяют, каким образом пользователи класса смогут создавать класс. Дополнительные сведения см. в статье Модификаторы доступа.

Конструктор можно объявить статическим, используя ключевое слово static. Статические конструкторы вызываются автоматически непосредственно перед доступом к статическим полям и обычно используются для инициализации членов статического класса. Дополнительные сведения см. в разделе Статические конструкторы.

Кроме обычных полей, методов, свойств классы и структуры могут иметь статические поля, методы, свойства. Статические поля, методы, свойства относятся ко всему классу/всей структуре и для обращения к подобным членам необязательно создавать экземпляр класса / структуры.

Статические поля

Статические поля хранят состояние всего класса / структуры. Статическое поле определяется как и обычное, только перед типом поля указывается ключевое слово static . Например, рассмотрим класс Person, который представляет человека:

В данном случае класс Person имеет два поля: age (хранит возраст человека) и retirementAge (хранит пенсионный возраст). Однако поле retirementAge является статическим. Оно относится не к конкретному человеку, а ко всем людям. (В данном случае для упрощения пренебрежем тем фактом, что в зависимости от пола и профессии пенсионный возраст может отличаться.) Таким образом, поле retirementAge относится не к отдельную объекту и хранит значение НЕ отдельного объекта класса Person, а относится ко всему классу Person и хранит общее значение для всего класса.

Причем в самом классе мы можем использовать это поле как и любые другие. Так, в методе СheckAge() , который поверяет пенсионный статус человека, для проверки используем это поле:

Но если мы хотим обратиться к этому полю вне своего класса, то мы можем обращаться к этому полю по имени класса:

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

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

Статические свойства

Подобным образом мы можем создавать и использовать статические свойства:

В данном случае доступ к статической переменной retirementAge опосредуется с помощью статического свойства RetirementAge .

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

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

В данном случае в классе Person счетчик хранится в приватной переменной counter, значение которой увеличивается на единицу при создании объекта в конструкторе. А с помощью статического свойства Counter, которое доступно только для чтения, мы можем получить значение счетчика.

Статические методы

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

В данном случае в классе Person определен статический метод CheckRetirementStatus() , который в качестве параметра принимает объект Person и проверяет его пенсионный статус.

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

Статический конструктор

Кроме обычных конструкторов у класса также могут быть статические конструкторы. Статические конструкторы имеют следующие отличительные черты:

Статические конструкторы не должны иметь модификатор доступа и не принимают параметров

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

Статические конструкторы нельзя вызвать в программе вручную. Они выполняются автоматически при самом первом создании объекта данного класса или при первом обращении к его статическим членам (если таковые имеются)

Статические конструкторы обычно используются для инициализации статических данных, либо же выполняют действия, которые требуется выполнить только один раз

Определим статический конструктор:

В данном случае с помощью встроенной структуры DateTime получаем текущий год. Для этого используется свойство DateTime.Now.Year . если он равен 2022, устанавливаем один пенсионный возраст. При другом значении текущего года устанавливается другое значение пенсионного возраста.

Статические классы

Статические классы объявляются с модификатором static и могут содержать только статические поля, свойства и методы. Например, определим класс, который выполняет ряд арифметических операций:

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

На уровне кода конструктор представляет метод, который называется по имени класса, который может иметь параметры, но для него не надо определять возвращаемый тип. Например, определим в классе Person простейший конструктор:

Конструкторы могут иметь модификаторы, которые указываются перед именем конструктора. Так, в данном случае, чтобы конструктор был доступен вне класса Person, он определен с модификатором public .

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

В данном случае выражение Person() как раз представляет вызов определенного в классе конструктора (это больше не автоматический конструктор по умолчанию, которого у класса теперь нет). Соответственно при его выполнении на консоли будет выводиться строка "Создание объекта Person"

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

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

Консольный вывод данной программы:

Ключевое слово this

Ключевое слово this представляет ссылку на текущий экземпляр/объект класса. В каких ситуациях оно нам может пригодиться?

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

первая часть - this.name означает, что name - это поле текущего класса, а не название параметра name. Если бы у нас параметры и поля назывались по-разному, то использовать слово this было бы необязательно. Также через ключевое слово this можно обращаться к любому полю или методу.

Цепочка вызова конструкторов

В примере выше определены три конструктора. Все три конструктора выполняют однотипные действия - устанавливают значения полей name и age. Но этих повторяющихся действий могло быть больше. И мы можем не дублировать функциональность конструкторов, а просто обращаться из одного конструктора к другому также через ключевое слово this , передавая нужные значения для параметров:

В данном случае первый конструктор вызывает второй, а второй конструктор вызывает третий. По количеству и типу параметров компилятор узнает, какой именно конструктор вызывается. Например, во втором конструкторе:

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

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

И если при вызове конструктора мы не передаем значение для какого-то параметра, то применяется значение по умолчанию.

Инициализаторы объектов

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

С помощью инициализатора объектов можно присваивать значения всем доступным полям и свойствам объекта в момент создания. При использовании инициализаторов следует учитывать следующие моменты:

С помощью инициализатора мы можем установить значения только доступных из вне класса полей и свойств объекта. Например, в примере выше поля name и age имеют модификатор доступа public, поэтому они доступны из любой части программы.

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

Инициализаторы удобно применять, когда поле или свойство класса представляет другой класс:

Обратите внимание, как устанавливается поле company :

Деконструкторы

Деконструкторы (не путать с деструкторами) позволяют выполнить декомпозицию объекта на отдельные части.

Например, пусть у нас есть следующий класс Person:

В этом случае мы могли бы выполнить декомпозицию объекта Person так:

Значения переменным из деконструктора передаюся по позиции. То есть первое возвращаемое значение в виде параметра personName передается первой переменной - name, второе возващаемое значение - переменной age.

По сути деконструкторы это не более,чем синтаксический сахар. Это все равно, что если бы мы написали:

При получении значений из декоструктора нам необходимо предоставить столько переменных, сколько деконструктор возвращает значений. Однако бывает, что не все эти значения нужны. И вместо возвращаемых значений мы можм использовать прочерк _ . Например, нам надо получить только возраст пользователя:

Поскольку первое возвращаемое значение - это имя пользователя, которое не нужно, в в данном случае вместо переменной прочерк.

В прошлой теме был разработан следующий класс:

И мы можем установить значения для переменных класса Person, можем получить их значения во внешние переменные. Однако если мы попробуем получить значения переменных name и age до их установки, то результаты будут неопределенными:

Чтобы избежать подобной ситуации применяются специальные функции инициализации или конструкторы. Они позволяют инициализировать объект класса. Так, изменим код программы следующим образом:

Теперь в классе Person определен конструктор:

По сути конструктор представляет функцию, которая может принимать параметры и которая должна называться по имени класса. В данном случае конструктор принимает два параметра и передает их значения полям name и age.

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

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

После этого вызова у объекта person для поля name будет определено значение "Tom", а для поля age - значение 22. Вполедствии мы также сможем обращаться к этим полям и переустанавливать их значения.

Тажке можно использовать сокращенную форму инициализации:

По сути она будет эквивалетна предыдущей.

Консольный вывод определенной выше программы:

Подобным образом мы можем определить несколько конструкторов и затем их использовать:

В классе Person определено три конструктора, и в функции все эти конструкторы используются для создания объектов:

Хотя пример выше прекрасно работает, однако мы можем заметить, что все три конструктора выполняют фактически одни и те же действия - устанавливают значения переменных name и age. И в C++ можем сократить их определения, вызова из одного конструктора другой и тем самым уменьшить объем кода:

Запись Person(string n): Person(n, 18) представляет вызов конструктора, которому передается значение параметра n и число 18. То есть второй конструктор делегирует действия по инициализации переменных первому конструктору. При этом второй конструктор может дополнительно определять какие-то свои действия.

Таким образом, следующее создание объекта

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

Инициализация констант и ссылок

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

Этот класс не будет компилироваться, так как здесь есть две ошибки - отсутствие инициализации константы name и ссылки ageRef. Хотяя их значения устанавливаются в конструкторе, но к моменту, когда код инструкции из тела конструктора начнут выполняться, константы и ссылки уже должны быть инициализированы. И для этого необходимо использовать списки инициализации:

Списки инициализации представляют перечисления инициализаторов для каждой из переменных и констант через двоеточие после списка параметров конструктора:

Таким образом, все переменные, константы и ссылки получат значение, и никакой ошибки не возникнет.

Читайте также: