Тема 4.2. Конструирование объектов 4.2.webp
Понятие конструктора и деструктора

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

Глобальные объекты по умолчанию инициализируются нулевыми значениями. Локальные объекты не имеют инициализирующих значений.

Конструктор

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

Пример:

421_01

В этой программе компилятор сам вызывает конструктор Student::Student() в том месте, где объявляется объект student. Тот же эффект имеет и создание объекта класса Student в куче.

Результат:

421_02

Конструктор можно создать и как обычную функцию с телом, вынесенным из объявления класса:

421_03

Конструирование массива объектов

Пример:

421_04

Каждый элемент массива конструируется отдельно:

421_05

Конструирование составных объектов

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

Пример:

421_06

421_07

Создание объекта repeat в main() автоматически вызывает конструктор Repetitor. Перед тем как управление будет передано телу конструктора Repetitor, вызываются конструкторы для объектов student и teacher. Конструктор Student вызывается первым, поскольку объект этого класса объявлен первым. Затем вызывается конструктор Teacher. Конструирование члена c класса Teacher (тип этого члена – Course) является частью процесса построения объекта класса Teacher. Каждый объект внутри класса должен быть сконструирован до того, как будет вызван конструктор класса контейнера (в противном случае этот конструктор не будет знать, в каком состоянии находятся члены-данные).

Только после создания всех этих объектов управление переходит к конструктору класса Repetitor, который теперь может конструировать оставшуюся часть объекта. Это не означает, что Repetitor отвечает за инициализацию Student и Teacher. Каждый класс отвечает за инициализацию своего объекта, где бы тот ни создавался.

Результат работы программы показывает, в каком порядке создаются объекты:

421_08

Деструктор

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

421_09

Деструктор имеет то же имя, что и класс, но с предшествующим ему символом тильды “ ~ ”. Деструктор не имеет типа возвращаемого значения.

Деструктор вызывается, когда объект выходит из области видимости. Локальный объект выходит из области видимости, когда функция, создавшая его, доходит до команды return. Глобальный или статический объект выходит из области видимости, когда прекращается работа программы.

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

421_10

Деструктор класса Student будет выглядеть так:

class Student

{

public:

Student()

{

// работает конструктор

}

~Student()

{

// Работает деструктор

}

};

int main()

{

Student student1;

Student student2;

return 0;

}

Областью видимости этого объекта является main(), поэтому объект уничтожается только после завершения работы main(), т.е. после команды return 0;

Если return 0; отсутствует, то объект все равно будет уничтожен, т.к. произошло завершение работы программы.

Последовательность действий:

  1. При создании объекта 1 запускается конструктор;
  2. При создании объекта 2 запускается конструктор;
  3. Выполняется остальная часть программы;
  4. в main() выполняется return 0;;
  5. Запускается деструктор для объекта 2;
  6. Запускается деструктор для объекта 1.

Еще один пример:

class Student

{

public:

Student() {}

~Student() {}

};

 

void fn()

{

Student student2;

}

 

int main()

{

Student student1;

fn();

Student student3;

}

Последовательность действий:

  1. При создании объекта 1 запускается конструктор;
  2. Вызывается функция fn();
  3. При создании объекта 2 запускается конструктор;
  4. Запускается деструктор для объекта 2;
  5. При создании объекта 3 запускается конструктор;
  6. Запускается деструктор для объекта 3;
  7. Запускается деструктор для объекта 1.

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

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

421_11

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

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

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

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

Область применения статических членов

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

Во-вторых, с помощью статических членов можно индицировать выполнение определенного действия. Например, классу Radio может понадобиться инициализировать некие аппаратные средства при первом выполнении команды tune, но не перед последующими вызовами. С помощью статического члена можно указать, что первый вызов tune уже выполнил инициализацию.

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

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

Инициализация статического свойства не может находиться внутри тела класса:

421_12

 Можно разместить ее в глобальной области, указав еще раз тип данных (здесь double):

421_13

Правила обращения к статическим свойствам те же, что и к обычным свойствам. Из класса к статическим членам можно обратиться так же, как и к другим членам класса. К открытым статическим членам можно обращаться извне класса, а к защищенным – нельзя, как и к обычным защищенным членам:

421_14

В строке 22 объект pupil, используемый для обращения к статическому свойству numPupil, никак не обрабатывается, даже если это обращение явным образом указано в выражении.

Более логичным является обращение статическому свойству через имя класса (строка 19).

Можно добавить “счетчик” объектов в конструктор, тогда при создании очередного нового объекта будет соответственно увеличиваться количество объектов:

421_15

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

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

421_16

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

421_17

С обычными свойствами можно:

421_18

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

421_19

Перегрузка конструктора

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

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

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

422_01_1 Корректный код: 422_01_2 Некорректный код:

В некорректном коде добавлен конструктор с параметром – это заставит C++ отказаться от автоматической генерации конструктора по умолчанию. Значит программист сам должен добавить конструктор без аргументов.

Конструктор с аргументами

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

422_02

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

Пример:

422_03

Пример с несколькими объектами:

422_04

Последовательность действий с несколькими объектами:

  1. при создании 1 объекта запускается конструктор;
  2. при создании 2 объекта запускается конструктор;
  3. при создании 3 объекта запускается конструктор;
  4. выполняется остальная часть программы;
  5. в main () выполняется return 0; (или отсутствует)
  6. запускается деструктор 3 объекта;
  7. запускается деструктор 2 объекта;
  8. запускается деструктор 1 объекта.

Результат:

422_05

В предыдущей программе областью действий объектов тоже являлась функция main(), поэтому деструктор начинал свою работу только перед завершением работы программы (или после команды return 0;).

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

422_06

Пример с классом Student:

422_07

В классе Student:

  • защищенное свойство класса *marks – указатель на массив, в котором хранится адрес нулевого элемента;
  • конструктор, в котором выделяется память для массива и инициализируется числом 5 весь массив;
  • деструктор, в котором память из-под массива возвращается в кучу.

В функции main():

создается объект, при этом в конструктор передается значение 3 для параметра size.

Перегрузка конструктора

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

Например, класс People может одновременно иметь три конструктора:

422_08

Ошибка попытки перегрузки:

422_09

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

Student noName;
Student Vanya ("Ivagov Ivan");
Student Petya ("Petrov Petr", 100, 4.5);

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

Создание класса с конструктором

1.

a) Написать программу, которая демонстрирует использование конструктора для двух объектов класса Dog.

Программа должна:

  • Выводить на экран сообщение о конструировании объектов;
  • Подсчитывать необходимое количество килограммов корма на 1 год для каждой собаки (количество корма на 1 порцию может вводиться пользователем или быть постоянным).

b) Изменить написанную ранее программу: создать массив из объектов класса Dogs.

2.

a) Класс House описать с конструктором, принимающим в качестве аргументов количество окон и количество дверей. Количество комнат задается в конструкторе равным 4. Создать объект. Результат вывести на экран:

423_01

b) Изменить описание класса House таким образом, чтобы в нем было несколько конструкторов. Создать несколько объектов. Вывести информацию на экран.

Делегирующий и копирующий конструкторы

Делегирующий конструктор

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

Пример. При создании объекта вызывается конструктор из 21 строки, который немедленно вызывает конструктор из 15 строки, который немедленно вызывает конструктор из 7 строки. Конструктор (7 строка) записывает “Иван” в firstname, а свойствам age и iq присваивает 0. Управление возвращается конструктору из 15 строки, который записывает в age значение 25. Далее управление возвращается конструктору из 21 строки, который в iq записывает 80:

424_01

Если появится необходимость выполнять какие-то дополнительные действия во время инициализации (например, добавить “Уважаемый ” к имени, то не нужно будет делать изменения в каждом конструкторе. Достаточно сделать изменения только в том конструкторе, где инициализируется свойство firstname:

424_02

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

424_03

Результат:

424_04

Конструирование членов класса

Класс может содержать члены-данные, которые являются объектами другого класса. В следующем примере реализована возможность вызывать конструктор класса Motor в процессе создания объекта класса Car:

424_05

Результат:

424_06

Последовательность конструирования объектов

1. Локальные и статические объекты создаются в том порядке, в котором они объявлены в программе.

2. Статические объекты создаются только один раз. Статические переменные сохраняют свое значение от вызова к вызову функции. В результате работы приведенной на следующем слайде программы сообщение от функции fn() появилось дважды, а сообщение от конструктора Dog – только при первом вызове fn():

424_07

В функции main() вызывается fn() с аргументом 12. В eda записывается 12 и выводится сообщение "Функции передано значение 12". Создается статический объект барбос и при этом вызывается конструктор класса Dog, который получает 12 из eda в korm. В конструкторе выводится "Dog сконструирован со значением 12". Далее идет возврат в fn() и после считывания там "}" управление возвращается в main(). Там снова вызывается fn() и выводится сообщение "Функции передано значение 34". Но барбос – статический объект и поэтому он больше не конструируется, следовательно сообщение из конструктора больше не выводится.

Результат:

424_08

3. Все глобальные объекты создаются до вызова функции main(). Тела конструкторов для всех глобальных объектов к моменту передачи управления main() уже выполнены, и если хоть один из конструкторов содержит ошибку, программа “погибнет” до того, как начнет выполняться. Поэтому можно либо проверять каждый конструктор на локальных объектах перед тем, как использовать его для глобальных; либо добавить команды вывода сообщений в начало всех конструкторов, которые предположительно могут иметь ошибки, и последнее сообщение, вероятно, будет сообщением конструктора с ошибкой.

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

Пример (фрагмент):

class Dog

{

public:

Dog (unsigned id) : dogID(id) {}

const int DogID;

};

 

class Owner

{

public:

Owner (Dog& d) : ownerID (d.dogID) {}

int ownerID;

};

Dog barbos (123); // Создали собаку

Owner ivanov (barbos); // Назначили собаке владельца

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

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

  • Owner Ivanov (Barbos); // Назначили собаке владельца
  • Dog Barbos (123); // Создали собаку

то объект Ivanov будет построен с использованием блока памяти, который пока что не является объектом типа Dog, а значит, вместо идентификатора собаки в  Ivanov будет находиться непредсказуемое значение. Во избежание этой ошибки не следует допускать, чтобы конструктор глобального объекта обращался к другому глобальному объекту.

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

Пример (фрагмент):

class Student

{

public:

Student (int id, int age) : sAge (age), sID (id) {}

const int sID;

const int sAge;

};

sID создается до sAge, несмотря на то, что он стоит вторым в инициализирующем списке конструктора.

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

Копирующий конструктор

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

Пример:

424_09

В этом примере автоматически работает неявный копирующий конструктор, который программистом не прописан. В этом примере выполняется побитовое копирование объекта.

Пример:

424_10

В момент  создания объекта (Dog barbos) этот barbos "поселился" по адресу 007EF8FF. После этого вызывается fn() и ей передается копия объекта – в этот момент запускается работа копирующего конструктора, который и создает эту копию, располагая ее по другому адресу, например, 007EF824. Деструктору, в свою очередь, приходится уничтожать два объекта в порядке, обратном их появлению: сначала барбосика, потом барбоса.

Результат:

424_11

Пример:

424_12

Здесь вызывается fn() с типом возвращаемого значения – Dog. Функция fn() создает объект temp (срабатывает конструктор). Затем  возвращает (пусть вникуда) копию объекта temp, т.е. в этом случае тоже срабатывает конструктор копирования. В этот момент (при срабатывании return) объект temp выходит из области видимости, поэтому для него запускается деструктор. Далее запускается деструктор для скопированного и возвращенного (пусть вникуда) объекта.

Результат:

424_13

Пример:

424_14

Описание проблемы с этой программой:

В классе Dog создано обычное свойство класса – указатель data на какое-то целочисленное значение. Затем в конструкторе, который принимает в качестве параметра размер массива, для свойства data создаваемого объекта выделяется память как для целочисленного массива (на три элемента), затем в цикле весь массив заполняется значением 25.

В main() создан объект barbos, для которого при этом без проблем выполняется работа конструктора.

Затем в main() без проблем создается еще один объект путем копирования барбоса. При этом автоматический конструктор копирования создает точную копию барбоса, а это значит, что и барбос, и шарик хранят один и тот же адрес (*data) на один и тот же массив.

После всего запускается деструктор, который начинает свою работу с удаления последнего созданного объекта – т.е. удаляет шарика вместе с массивом, на который указывает *data.

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

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

424_15

424_16

424_17

Результат:

424_18


Работа с деструктором
  1. Добавить деструктор в описание класса Dog.
  2. Добавить деструктор в описание класса House.
Тренажёр