Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Программирование на C / C++ / C++ for real programmers.pdf
Скачиваний:
262
Добавлен:
02.05.2014
Размер:
2.04 Mб
Скачать

Ведущие

6

 

указатели и

 

дескрипторы

 

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

Семантика ведущих указателей

При работе с умными указателями имеется один важный частный случай — когда два умных указателя не должны одновременно ссылаться на один объект. Между указателем и объектом, на который он ссылается, существует однозначное соответствие (за исключением особого случая умных указателей, ссылающихся на NULL). Если в программном дизайне действует такое ограничение, говорят, что реализуется семантика ведущих указателей (master pointers).

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

1. Указываемые объекты должны создаваться указателями в конструкторах.

2.Деструктор указателя должен удалять указываемый объект.

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

4.Оператор присваивания operator= должен удалять текущий указываемый объект, находящийся слева от него, и заменять его копией указываемого объекта справа.

Кроме того, было бы разумно сделать еще две вещи:

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

6.Создавать указатели с помощью производящих функций (factory functions).

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

template <class Type> class MP {

private: Type* t;

public:

86

MP();

// Создает

указываемый объект

MP(const MP<Type>&);

// Копирует указываемый объект

~MP();

// Удаляет

указываемый объект

MP<Type>& operator=(const MP<Type>&); // Удаляет левосторонний объект, // копирует правосторонний

Type* operator->() const;

};

Конструирование

Предположим, вы разрешили пользователям создавать собственные указываемые объекты и передавать их ведущим указателям во время конструирования.

template <class Type> class MP {

private: Type* t;

public:

MP() : t(NULL) {}

MP(Type* pointer) : t(pointer) {} ~MP() { delete t; }

// И т.д.

};

Foo* foo = new Foo; MP<Foo> mp1(foo);

MP<Foo> mp2(foo); // Облом!

Насколько проще была бы жизнь без таких пользователей! Когда mp1 удаляется, пропадает и foo. В итоге mp2 начинает указывать неизвестно куда. «Зачем кому-нибудь потребуется вытворять такое?» — спросите вы. Как говорилось в одном рекламном ролике: «Зачем спрашивать «Зачем»?» Если вы оставите такую дыру, можете не сомневаться: кто-нибудь когда-нибудь изобретет дьявольский план, использует ее и обвинит во всех смертных грехах вашу программу.

Пользователь прямо-таки кричит: «Держите меня, пока я не натворил бед». Для этого существует надежный способ: отобрать у него ключи от конструкторов класса указываемого объекта.

class Foo {

friend class MP<Foo>;

protected:

 

Foo();

// Теперь доступ к конструктору имеет только MP<Foo>

public:

 

// Оставшаяся часть интерфейса

};

template <class Type> class MP {

private: Type* t;

public:

MP() : t(new Type) {} // И т.д.

};

Aгa, уже лучше. При создании указателя его конструктор также конструирует и указываемый объект. Объявляя указатель другом, мы можем сделать конструкторы Foo закрытыми или защищенными и

87

сохранить доступ к ним из конструкторов указателя. Теперь клиент никак не сможет добраться до Foo, кроме как через MP<Foo>. Мы еще неоднократно вернемся к вопросу о том, как, когда и где создавать указываемые объекты, а пока давайте немного отвлечемся.

Если конструкторы Foo вызываются с аргументами, существуют две альтернативы:

1. Вместо того чтобы пользоваться универсальным шаблоном ведущего указателя, создайте для Foo нестандартный класс ведущего указателя MPFoo. Для каждого конструктора Foo создайте конструктор MPFoo с точно такой же сигнатурой и передайте его аргументы конструктору Foo.

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

Второй вариант выглядит так:

class Foo {

friend class MP<Foo>;

protected:

 

Foo();

// Единственный конструктор

public:

 

Initialized(int, char*);

// Оставшаяся часть интерфейса

};

MP<Foo> mpf;

mpf->Initialize(17, “Hello”); // Завершить конструирование

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

Уничтожение

Нет ничего проще: в деструкторе ведущего указателя удаляется и указываемый объект.

template <class Type> class MP {

private: Type* t;

public:

~MP() { delete t; }

};

Копирование

Ой! Опять эти проклятые пользователи...

MP<Foo>

mpf1;

//

Создает Foo, на который ссылается указатель

MP<Foo>

mpf2 = mpf1;

//

Неудача!

Пусть знак равенства не вводит вас в заблуждение — здесь происходит конструирование, и эта строка эквивалентна строке MP<Foo> mpf2(mpfl);. Если не перегрузить конструктор копий и разрешить компилятору C++ внести свою лепту, мы получим два ведущих указателя, которые ссылаются на один и тот же объект Foo. По умолчанию конструктор копий, генерируемый компилятором, радостно копирует содержащийся в переменной адрес из старого указателя в новый. Проблема решается относительно просто.

template <class Type> class MP {

88

private:

 

Type* t;

 

public:

 

MP();

// Нормальный

MP(const MP<Type>& mp) : t(*(mp.t)) {}

// Конструктор копий

};

 

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

template <class Type>

 

class MP {

 

private:

 

Type* t;

 

MP(const MP<Type>&) : t(NULL) {}

// Никогда не будет вызываться

public:

 

MP();

 

};

 

Тем самым мы предотвращаем непреднамеренное копирование в ситуациях вроде следующей:

void f(MP<Foo>);

MP<Foo>

mpf;

f(mpf);

// Создается временная копия

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

Присваивание

Ааааа! Эти зловредные пользователи когда-нибудь угомонятся?!

MP<Foo> mpf1;

 

MP<Foo> mpf2;

 

mpf2 = mpf1;

// Нет, только не это...

В приведенном фрагменте возникают сразу две проблемы. Во-первых, указываемый объект, созданный конструктором mpf2, никогда не удаляется. Он превращается в Летучего Голландца, обреченного на вечные скитания в океане памяти. Во-вторых, оператор =, используемый компилятором по умолчанию, копирует адрес, находящийся в t, из одного указателя в другой, что приводит к появлению двух ведущих указателей, ссылающихся на один объект. В исправленном варианте перегруженный оператор = удаляет объект, на который ссылается левосторонний указатель, и заменяет его копией объекта, на который ссылается правосторонний указатель.

template <class Type> class MP {

private: Type* t;

public:

MP(); // Нормальный конструктор

MP<Type>& operator=(const MP<Type>& mp)

{

if (&mp != this) { delete t;

t = new Type(*(mp.t));