Программирование >>  Синтаксис инициирования исключений 

1 ... 59 60 61 [ 62 ] 63 64 65 ... 82


union U {

Foo foo; Bar bar; Banana banana;

U whatIsThis;

Компилятор С++ не может определить, какой конструктор следует вызывать для whatIsThis - Foo::Foo() , Bar::Bar() или Banana::Banana() . Разумеется, больше одного конструктора вызывать нельзя, поскольку все члены занимают одно и то же место в памяти, но без инструкций от вас не может выбрать нужный конструктор. Как и во многих других ситуациях, компилятор поднимет руки; он сообщает об ошибке и отказывается принимать объединение, члены которого имеют конструкторы. Если вы хотите, чтобы одна область памяти могла инициализироваться несколькими различными способами, придется подумать, как обмануть компилятор. Описанный выше пустой конструктор подойдет лучше всего.

unsigned char space[4096];

Foo* whatIsThis = new(&space[0]) Foo;

Фактически происходит то, что в С++ происходить не должно - вызов конструктора. При этом память на выделяется и не освобождается, поскольку оператор new ничего не делает. Тем не менее, компилятор С++ сочтет, что это новый объект, и все равно вызовет конструктор. Если позднее вы передумаете и захотите использовать ту же область памяти для другого объекта, то сможете снова вызвать хитроумный оператор new и инициализировать ее заново.

При создании объекта оператором new компилятор всегда использует двухшаговый процесс:

1 . Выделение памяти под объект.

2. Вызов конструктора объекта.

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

Оперативное изменение типа объекта

Если позднее Foo вам надоест и вы захотите использовать ту же область для Banana, то при наличии у Banana того же перегруженного оператора new вы сможете быстро сменить тип объекта.

Banana* b = new(&space[0]) Banana;

Пуф! Был Foo, стал Banana. Это и называется идиомой виртуального конструктора. Такое решение полностью соответствует спецификации языка.

Ограничения

Применяя эту идиому, необходимо помнить о двух обстоятельствах:

1 . Область, передаваемая оператору new, должна быть достаточна для конструирования класса.

2. Об изменении должны знать все клиенты, хранящие адрес объекта!

Будет весьма неприятно, если вы сменили тип объекта с Foo на Banana только для того, чтобы какой-нибудь клиентский объект тут же вызвал одну из функций Foo.

Уничтожение с разделением фаз

Объект, переданный в качестве аргумента оператору del ete, обычно уничтожается компилятором в два этапа:

1 . Вызов деструктора.

2. Вызов оператора delete для освобождения памяти.

Довольно часто мы качаем головой и говорим: Хорошо бы вызвать деструктор, но не трогать память . Допустим, вы разместили объект в пуле, а теперь не хотите, чтобы часть локально созданного пула



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

void f() {

Pool localPool;

Foo* foo1 = new Foo; Использует оператор new по умолчанию Foo* foo2 = new(&1ocalPoo1) Foo; Использует перегрузку

delete fool; Для оператора new по умолчанию

foo2->~Foo(); Прямой вызов деструктора

localPool - большой блок памяти, локальный по отношению к функции. Поскольку он создается в стеке, при завершении f() он выталкивается из стека. Выделение происходит молниеносно, поскольку локальные объекты заполняют пул снизу вверх. Освобождение происходит еще быстрее, поскольку уничтожается сразу весь пул. Единственная проблема заключается в том, что компилятор не будет автоматически вызывать деструкторы объектов, созданных внутри localPool. Вам придется сделать это самостоятельно, используя только что описанную методику.

Кто управляет выделением памяти?

Довольно разговоров о конкретных механизмах; поговорим об архитектуре. Существуют три основные стратегии для определения того, где объект будет находиться в памяти и как занимаемая им память в конечном счете возвратится в систему:

1. Глобальное управление.

2. Управление в классах.

3. Управление под руководством клиента.

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

Глобальное управление

По умолчанию объекты создаются глобальным оператором new и уничтожаются глобальным оператором delete. Перегрузка этих операторов позволяет вам реализовать нестандартную схему управления памятью, но это считается дурным тоном.

Очень трудно объединить раздельно написанные библиотеки, каждая из которых перегружает заданные по умолчанию операторы new и delete.

Ваши перегрузки влияют не только на ваш код, но и на код, написанный другими (включая библиотеки, для которых нет исходных текстов).

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

Пользователи могут изменить вашу предположительно глобальную стратегию, перегружая операторы new и delete в конкретных классах. Перегрузка стандартных глобальных операторов дает меньше, чем хотелось бы.



Выделение и освобождение памяти в классах

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

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

Управление памятью под руководством клиента

Как демонстрируют приведенные выше фрагменты, клиентский код может выбирать, где объект должен находиться в памяти. Обычно это делается с помощью перегруженного оператора new, имеющего дополнительные аргументы помимо size t. В управлении памятью открываются новые перспективы - управление на уровне отдельных объектов, а не класса в целом. К сожалению, эта стратегия перекладывает на клиента хлопоты, связанные с освобождением памяти. Реализация получается сложной, а модульность - низкой. Например, стоит изменить аргументы нестандартного оператора new, и вам придется вносить изменения во всех местах клиентского кода, где он используется. Пока все перекомпилируется заново, можно погулять на свежем воздухе. Впрочем, несмотря на все проблемы, эта стратегия легко реализуема, очень эффективна и хорошо работает в простых ситуациях.

Объекты классов и производящие функции

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

Управление памятью с применением ведущих указателей

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

Специализированные ведущие указатели

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

struct Pool { ... }; Как и раньше

void* operator new(Poo1* p); Выделение из пула

template <c1ass Type>

class PoolMP {

private:

Type* pointee;

PoolMP(const PoolMP<Type>&) {} Копирование не разрешено... PoolMP<Type>& operator=(const PoolMP<Type>&)

{ return *this; } ...и присваивание тоже

public:



1 ... 59 60 61 [ 62 ] 63 64 65 ... 82

© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки.
Яндекс.Метрика