• Чтобы отключить функциональность, автоматически предоставляемую компилятором, объявите соответствующую функцию-член закрытой и не включайте ее реализацию. Наследование базовому классу типа Uncopyable – один из способов сделать это.
Правило 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 ищется та реальная функция, которую нужно вызвать.