Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе

Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе

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

class TimeKeeper {

public:

TimeKeeper();

~TimeKeeper();

...

};

class AtomicClock: public TimeKeeper {…};

class WaterClock: public TimeKeeper {….};

class WristWatch: public TimeKeeper {…};

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

TimeKeeper *getTimeKeeper(); // возвращает указатель на динамически

// выделенный объект класса,

// производного от TimeKeeper

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

TomeKeeper *ptk = getTimeKeeper(); // получить динамически выделенный

// объект из иерархии TimeKeeper

... // использовать его

delete ptk; // уничтожить, чтобы избежать утечки

// ресурсов

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

Проблема в том, что getTimeKeeper возвращает указатель на объект производного класса (например, AtomicClock), а удалять этот объект нужно через указатель на базовый класс (то есть на TimeKeeper), при этом в базовом классе (TimeKeeper) объявлен невиртуальный деструктор. Это прямой путь к неприятностям, потому что в спецификации C++ постулируется, что когда объект производного класса уничтожается через указатель на базовый класс с невиртуальным деструктором, то результат не определен. Во время исполнения это обычно приводит к тому, что часть объекта, принадлежащая производному классу, никогда не будет уничтожена. Если getTimeKeeper() возвращает указатель на объект класс AtomicClock, то часть объекта, принадлежащая AtomicClock (то есть данные-члены, объявленные в этом классе), вероятно, не будут уничтожены, так как не будет вызван деструктор AtomicClock. Те же члены, что относятся к базовому классу (то есть TimeKeeper), будут уничтожены, что приведет к появлению так называемых «частично разрушенных» объектов. Это верный путь к утечке ресурсов, повреждению структур данных и проведению изрядного времени в обществе отладчика.

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

class TimeKeeper {

public:

TimeKeeper();

virtual ~TimeKeeper();

...

};

TimeKeeper *ptk = get TimeKeeper();

...

delete ptk; // теперь работает правильно

Обычно базовые классы вроде TimeKeeper содержат и другие виртуальные функции, кроме деструктора, поскольку назначение виртуальных функций – обеспечить возможность настройки производных классов (см. правило 34). Например, в классе TimeKeeper может быть определена виртуальная функция getCurrentTime, реализованная по-разному в разных производных классах. Любой класс с виртуальными функциями почти наверняка должен иметь виртуальный деструктор.

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

class Point { // точка на плоскости

public:

Point(int xCoord, int yCoord);

~Point();

private:

int x,y;

};

Если int занимает 32 бита, то объект Point обычно может поместиться в 64-битовый регистр. Более того, такой объект Point может быть передан как 64-битовое число функциям, написанным на других языках (таких как C или FORTRAN). Если же деструктор Point сделать виртуальным, то ситуация изменится.

Для реализации виртуальных функций необходимо, чтобы в объекте хранилась информация, которая во время исполнения позволяет определить, какая виртуальная функция должна быть вызвана. Эта информация обычно представлена указателем на таблицу виртуальных функций vptr (virtual table pointer). Сама таблица – это массив указателей на функции, называемый vtbl (virtual table). С каждым классом, в котором определены виртуальные функции, ассоциирована таблица vtbl. Когда для некоторого объекта вызывается виртуальная функция, то с помощью указателя vptr в таблице vtbl ищется та реальная функция, которую нужно вызвать.

Детали реализации виртуальных функций не важны. Важно то, что если класс Point содержит виртуальную функцию, то объект этого типа увеличивается в размере. В 32-битовой архитектуре его размер возрастает с 64 бит (два целых int) до 96 бит (два int плюс vptr); в 64-битовой архитектуре он может вырасти с 64 до 128 бит, потому что указатели в этой архитектуре имеют размер 64 бита. Таким образом, добавление vptr к объекту Point увеличивает его размер на величину от 50 до 100 %! После этого объект Point уже не может поместиться в 64-битный регистр. Более того, объекты этого типа в C++ перестают выглядеть так, как аналогичные структуры, объявленные на других языках, например на C, потому что в других языках нет понятия vptr. В результате становится невозможно передавать объекты типа Point написанным на других языках программам, если только вы не учтете наличия vptr. А это уже деталь реализации, и, следовательно, такой код не будет переносимым.

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

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

class SpecialString: public std::string { // плохо! std::string содержит

... // невиртуальный деструктор

};

На первый взгляд такой код может показаться безвредным, но если где-то в приложении вы преобразуете указатель на SpecialString в указатель на string, а затем выполните для этого указателя delete, то немедленно попадете в область неопределенного поведения:

SpecialString *pss = new SpecialString(“Надвигающаяся опасность”);

std::string *ps;

...

ps = pss; // SpecialString*=>std::string*

...

delete ps; // неопределенность! На практике ресурсы, выделенные

// объекту SpecialString, не будут освобождены, потому

// что деструктор SpecialString не вызывается

То же относится к любому классу, в котором нет виртуального деструктора, в частности ко всем типам STL-контейнеров (например, vector, list, set, tr1::unordered_map [см. правило 54] и т. д.). Если у вас когда-нибудь возникнет соблазн унаследовать стандартному контейнеру или любому другому классу с невиртуальным деструктором, воздержитесь! (К сожалению, в C++ не предусмотрено никакого механизма предотвращения наследования, как, скажем, final в языке Java, или sealed в C#).

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

class AWOV { // AWOV = “Abstract w/o Virtuals”

public:

virtual ~AWOV() = 0; // объявление чисто виртуального

}; // деструктора

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

AWOV::~AWOV(){}; // определение чисто виртуального деструктора

Дело в том, что сначала всегда вызывается деструктор «самого производного» класса (то есть находящегося на нижней ступени иерархии наследования), а затем деструкторы каждого базового класса. Компилятор сгенерирует вызов ~AWOV из деструкторов производных от него классов, а значит, вы должны позаботиться о его реализации. Если этого не сделать, компоновщик будет недоволен.

Правило включения в базовые классы виртуальных деструкторов касается только полиморфных базовых классов, то есть таких, которые позволяют манипулировать объектами производных классов с помощью указателя на базовый. TimeKeeper – полиморфный базовый класс, мы ожидаем, что при наличии указателя на объект TimeKeeper сможем манипулировать объектами AtomicClock и WaterClock.

Не все базовые классы разрабатываются с учетом полиморфизма. Например, и стандартный тип string, и типы STL-контейнеров спроектированы так, что не допускают возможности использования в качестве базовых, так как не являются полиморфными. Некоторые классы предназначены служить в качестве базовых, но полиморфно использоваться не могут; примером могут служить класс Uncopyable из правила 6 и класс input_iterator_tag из стандартной библиотеки (см. правило 47). Таким классам не нужны виртуальные деструкторы.

Что следует помнить

• Полиморфные базовые классы должны объявлять виртуальные деструкторы. Если класс имеет хотя бы одну виртуальную функцию, он должен иметь виртуальный деструктор.

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

Данный текст является ознакомительным фрагментом.



Поделитесь на страничке

Следующая глава >

Похожие главы из других книг:

Работа с виртуальными дисками

Из книги автора

Работа с виртуальными дисками Особо следует отметить возможности Virtual PC 2004 по работе с виртуальными жесткими дисками. К таковым, в частности, относятся возможность подключения к каждой ВМ до трех виртуальных жестких дисков и поддержка трех типов таких дисков: диска


Работа с виртуальными дисками

Из книги автора

Работа с виртуальными дисками Возможности VMware по работе с виртуальными жесткими дисками по «численным показателям» превышают возможности Virtual PC 2004, В частности, вы можете подключить к каждой ВМ до четырех виртуальных жестких дисков с интерфейсом IDE и до семи дисков с


18. Объявляйте переменные как можно локальнее

Из книги автора

18. Объявляйте переменные как можно локальнее РезюмеИзбегайте "раздувания" областей видимости. Переменных должно быть как можно меньше, а время их жизни — как можно короче. Эта рекомендация по сути является частным случаем рекомендации 10.ОбсуждениеПеременные, время


50. Делайте деструкторы базовых классов открытыми и виртуальными либо защищенными и невиртуальными

Из книги автора

50. Делайте деструкторы базовых классов открытыми и виртуальными либо защищенными и невиртуальными РезюмеУдалять или не удалять — вот в чем вопрос! Если следует обеспечить возможность удаления посредством указателя на базовый класс, то деструктор базового класса


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

Из книги автора

54. Избегайте срезки. Подумайте об использовании в базовом классе клонирования вместо копирования РезюмеСрезка объектов происходит автоматически, невидимо и может приводить к полному разрушению чудесного полиморфного дизайна. Подумайте о полном запрете копирующего


Правило 8: Не позволяйте исключениям покидать деструкторы

Из книги автора

Правило 8: Не позволяйте исключениям покидать деструкторы C++ не запрещает использовать исключения в деструкторах, но это, безусловно, очень нежелательная практика. На то есть серьезная причина. Рассмотрим пример:class Widget {public:...~Widget() {...} // предположим, здесь есть


Правило 22: Объявляйте данные-члены закрытыми

Из книги автора

Правило 22: Объявляйте данные-члены закрытыми В этом правиле мы поговорим о том, почему данные-члены не должны быть открытыми (public). Затем мы убедимся, что все аргументы против открытых данных-членов касаются также защищенных (protected). Это приведет нас к выводу, что


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

Из книги автора

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


Несколько слов о классе System.Environment

Из книги автора

Несколько слов о классе System.Environment Давайте рассмотрим класс System.Environment подробнее. Этот класс содержит ряд статических членов, позволяющих получить информацию относительно операционной системы, в которой выполняется .NET-приложение. Чтобы иллюстрировать возможности этого


Ввод и вывод в классе Console

Из книги автора

Ввод и вывод в классе Console Вдобавок к членам, указанным в табл. 3.2, тип Console определяет множество методов, обрабатывающих ввод и вывод, причем все эти методы определены как статические (static), поэтому они вызываются на уровне класса. Вы уже видели, что WriteLine() вставляет


Работа с виртуальными каталогами IIS

Из книги автора

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


17.5. Виртуальные функции в базовом и производном классах

Из книги автора

17.5. Виртуальные функции в базовом и производном классах По умолчанию функции-члены класса не являются виртуальными. В подобных случаях при обращении вызывается функция, определенная в статическом типе объекта класса (или указателя, или ссылки на объект), для которого она


Информация о классе объекта (структура CRuntimeClass)

Из книги автора

Информация о классе объекта (структура CRuntimeClass) Во многих случаях бывает необходимо уже во время работы приложения получить информацию о классе объекта и его базовом классе. Для этого любой класс, наследованный от базового класса CObject связан с структурой CRuntimeClass. Она


Информация о классе

Из книги автора

Информация о классе Класс CObject содержит два метода: GetRuntimeClass и IsKindOf, позволяющих получить информацию о классе объекта.Виртуальный метод GetRuntimeClassВиртуальный метод GetRuntimeClass возвращает указатель на структуру CRuntimeClass, описывающую класс объекта, для которого метод был


Что на самом деле происходит при полиморфном присоединении?

Из книги автора

Что на самом деле происходит при полиморфном присоединении? Все сущности, встречающиеся в предыдущих примерах полиморфных присваиваний, имеют тип ссылок: возможными значениями p, r и t являются не объекты, а ссылки на объекты. Поэтому результатом присваивания p := r является


Nokia 3500 Classic: Два мегапиксела в среднем классе

Из книги автора

Nokia 3500 Classic: Два мегапиксела в среднем классе Автор: Алексей Стародымов+приятный внешний вид,2-мегапиксельная камера,microSD на 128 Мбайт в комплекте -зернистый экран невысокого разрешения, отсутствует ИК-порт, в комплект не входит miniUSB- кабельNokia 3500 Classic, пожалуй, первый