Выбрать главу

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