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

Предыдущая  Содержание  Следующая V*D*V

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

За создание объекта отвечает new(), а вернуть ресурсы собственнику должна delete(). new() знает, какой вид объекта она создаёт, так как получает описание объекта в качестве первого параметра. Используя цепочку операторов if на основе данного параметра можно было бы обрабатывать создание каждого типа в отдельности. Недостаток заключается в том, что new() будет содержать код для каждого поддерживаемого типа данных в явном виде.

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

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

Теперь есть проблема в new(). Она отвечает за создание объектов и возвращения указателей, которые могут быть переданы в delete(), то есть new() должна установить в каждом объект информацию о деструкторе. Очевидный подход — сделать указатель на деструктор частью дескриптора типа, который передаётся new(). Пока нам нужно что-то вроде следующих деклараций:

 

struct type {

    size_t size;            /* размер объекта */

    void (* dtor) (void *); /* деструктор */

};

 

struct String {

    char * text;            /* динамическая строка */

    const void * destroy;   /* место для деструктора */

};

 

struct Set {

    ... информация ...

    const void * destroy;   /* место для деструктора */

};

 

Похоже, есть ещё одна проблема: кто-то должен скопировать указатель на деструктор dtor из описания типа в поле destroy нового объекта и эту копию, возможно, придётся размещать в разные места каждого класса объектов.

Инициализация является частью работы new() и разные типы требуют разной работы — new() могут даже потребоваться разные аргументы для разных типов:

 

new(Set);            /* создать набор */

new(String, "text"); /* создать строку */

 

Для инициализации мы используем другую зависимую от типа функцию, которую будем называть конструктор. Так как конструктор и деструктор зависят от типа и не изменяются, передаём их обоих в new() как часть описания типа.

Обратите внимание, что конструктор и деструктор не несут ответственности за получение и освобождение памяти для самого объекта — это работа new() и delete(). Конструктор вызывается в new() и несёт ответственность только за инициализацию области памяти, выделенную new(). Для строки это действительно влечёт запрос другого куска памяти для хранения текста, но пространство для самой struct String выделяется new(). Это пространство позже освобождается delete(). Однако, прежде чем delete() освободит область памяти, выделенную new(), первым делом delete() вызывает деструктор, который в основном выполняет работу обратную инициализации, сделанной конструктором.

 

Предыдущая  Содержание  Следующая