Б. Страуструп - Язык программирования С++ (1119446), страница 101
Текст из файла (страница 101)
Блокировка всех операций, как это делается в мониторах некоторыхоперационных систем, является избыточной и может существенно ухудшить параллельный режимвыполнения.Переопределив все функции интерфейса в управляющем классе, мы получили по сравнению сприемом перегрузки операции ->, то преимущество, что теперь можно строить производные отset_controller классы. К сожалению, мы можем потерять и некоторые достоинства управляющего класса,если к производным классам будут добавляться члены, представляющие данные. Можно сказать, чтопрограммный объем, который разделяется между управляемыми классами уменьшается по мере ростапрограммного объема управляющего класса.13.10 Управление памятьюПри проектировании библиотеки или просто программы с большим временем счета один из ключевыхвопросов связан с управлением памятью.
В общем случае создатель библиотеки не знает, в какомокружении она будет работать. Будет ли там ресурс памяти настолько критичен, что ее нехватка станетсерьезной проблемой, или же серьезной помехой станут накладные расходы, связанные с управлениемпамятью?Один из основных вопросов управления памятью можно сформулировать так: если функция f()передает или возвращает указатель на объект, то кто должен уничтожать этот объект? Необходимоответить и на связанный с ним вопрос: в какой момент объект может быть уничтожен? Ответы на этивопросы особенно важны для создателей и пользователей таких контейнеров, как списки, массивы иассоциативные массивы. С точки зрения создателя библиотеки идеальными будут ответы: "Система" и"В тот момент, когда объект больше никто не использует".
Когда система уничтожает объект, обычноговорят, что она занимается сборкой мусора, а та часть системы, которая определяет, что объектбольше никем не используется, и уничтожает его, называется сборщиком мусора.К сожалению, использование сборщика мусора может повлечь за собой накладные расходы на времясчета и память, прерывания полезных функций, определенную аппаратную поддержку, трудностисвязывания частей программы на разных языках или просто усложнение системы.
Многие пользователине могут позволить себе этого. Говорят, что программисты на Лиспе знают, насколько важноуправление памятью, и поэтому не могут отдать его пользователю. Программисты на С тоже знают,насколько важно управление памятью, и поэтому не могут оставить его системе.Поэтому в большинстве программ на С++ не приходится рассчитывать на сборщик мусора и нужнопредложить свою стратегию размещения объектов в свободной памяти, не обращаясь к системе. Нореализации С++ со сборщиком мусора все-таки существуют.Рассмотрим самую простую схему управления памятью для программ на С++.
Для этого заменимoperator new() на тривиальную функцию размещения, а operator delete() - на пустую функцию:inline size_t align(size_t s)/*Даже в простой функции размещения нужно выравнивание памяти, чтобы наобъект можно было настроить указатель произвольного типа*/{361Бьерн Страуструп.Язык программирования С++union Word { void* p; long double d; long l; }int x = s + sizeof(Word) - 1;x -= x%sizeof(Word);return x;}static void* freep; // настроим start на свободную памятьvoid* operator new(size_t s) // простая линейная функция размещения{void* p = freep;s = align(s);freep += s;return p;}void operator delete(void*) { } // пустоЕсли память бесконечна, то наше решение дает сборщик мусора без всяких сложностей и накладныхрасходов. Такой подход не применим для библиотек, когда заранее неизвестно, каким образом будетиспользоваться память, и когда программа, пользующаяся библиотекой, будет иметь большое времясчета.
Такой способ выделения памяти идеально подходит для программ, которым требуетсяограниченный объем памяти или объем, пропорциональный размеру входного потока данных.13.10.1 Сборщик мусораСборку мусора можно рассматривать как моделирование бесконечной памяти на памяти ограниченногоразмера. Помня об этом, можно ответить на типичный вопрос: должен ли сборщик мусора вызыватьдеструктор для тех объектов, память которых он использует? Правильный ответ - нет, поскольку, еслиразмещенный в свободной памяти объект не был удален, то он не будет и уничтожен. Исходя из этого,операцию delete можно рассматривать как запрос на вызов деструктора (и еще это - сообщениесистеме, что память объекта можно использовать). Но как быть, если действительно требуетсяуничтожить размещенный в свободной памяти объект, который не был удален? Заметим, что длястатических и автоматических объектов такой вопрос не встает, - деструкторы для них неявновызываются всегда.
Далее, уничтожение объекта "во время сборки мусора" по сути является операциейс непредсказуемым результатом. Она может совершиться в любое время между последнимиспользованием объекта и "концом программы", а значит, в каком состоянии будет программа в этотмомент неизвестно. Здесь использованы кавычки, потому что трудно точно определить, что такое конецпрограммы.
(прим. перев.)Трудно правильно запрограммировать такие операции и они не так полезны, как кажется.Задачу уничтожения объектов, если время этой операции точно не задано, можно решить с помощьюпрограммы обслуживания заявок на уничтожение. Назовем ее сервером заявок. Если объектнеобходимо уничтожить в конце программы, то надо записать в глобальный ассоциативный массив егоадрес и указатель на функцию "очистки". Если объект удален явной операцией, заявка аннулируется.При уничтожении самого сервера (в конце программы) вызываются функции очистки для всехоставшихся заявок.
Это решение подходит и для сборки мусора, поскольку мы рассматриваем ее какмоделирование бесконечной памяти. Для сборщика мусора нужно выбрать одно из двух решений: либоудалять объект, когда единственной оставшейся ссылкой на него будет ссылка, находящаяся в массивесамого сервера, либо (стандартное решение) не удалять объект до конца программы, поскольку всетаки ссылка на него есть.Сервер заявок можно реализовать как ассоциативный массив ($$8.8):class Register {Map<void*, void (*) (void*)> m;public:insert(void* po, void(*pf)()) { m[po]=pf; }remove(void* po) { m.remove(po); }};Register cleanup_register;362Бьерн Страуструп.Язык программирования С++Класс, постоянно обращающийся к серверу, может выглядеть так:class X {// ...static void cleanup(void*);public:X(){cleanup_register.insert(this,&cleanup);// ...}~X() { cleanup(this); }// ...};void X::cleanup(void* pv){X* px = (X*)pv;cleanup_register.remove(pv);// очистка}Чтобы в классе Register не иметь дела с типами, мы использовали статическую функцию-член суказателем типа void*.13.10.2 Контейнеры и удалениеДопустим, что у нас нет бесконечной памяти и сборщика мусора.
На какие средства управленияпамятью может рассчитывать создатель контейнера, например, класса Vector? Для случая такихпростых элементов, как int, очевидно, надо просто копировать их в контейнер. Столь же очевидно, чтодля других типов, таких, как абстрактный класс Shape, в контейнере следует хранить указатель.Создатель библиотеки должен предусмотреть оба варианта. Приведем набросок очевидного решения:template<class T> Vector {T* p;int sz;public:Vector(int s) { p = new T[sz=s]; }// ...};Если пользователь не будет заносить в контейнер вместо указателей на объекты сами объекты типаShape, то это решение подходит для обоих вариантов.Vector<Shape*> vsp(200);Vector<Shape> vs(200);// нормально// ошибка при трансляцииК счастью, транслятор отслеживает попытку создать массив объектов абстрактного базового классаShape.Однако, если используются указатели, создатель библиотеки и пользователь должны договориться, ктобудет удалять хранимые в контейнере объекты.
Рассмотрим пример:void f()// противоречивое использование средств// управления памятью{Vector<Shape*> v(10);Circle* cp = new Circle;v[0] = cp;v[1] = new Triangle;Square s;363Бьерн Страуструп.v[2] = &s;delete cp;Язык программирования С++// не удаляет объекты, на которые настроены// указатели, находящиеся в контейнере}Если использовать реализацию класса Vector из $$1.4.3, объект Triangle в этом примере навсегдаостанется в подвешенном состоянии (на него нет указателей), если только нет сборщика мусора.Главное в управлении памятью это - это корректность.
Рассмотрим такой пример:void g()// корректное использование средств управления памятью{Vector<Shape*> v(10);Circle* cp = new Circle;v[0] = cp;v[1] = new Triangle;Square s;v[2] = &s;delete cp;delete v[1];}Рассмотрим теперь такой векторный класс, который следит за удалением занесенных в негоуказателей:template<class T> MVector {T* p;int sz;public:MVector(int s);~MVector();// ...};template<class T> MVector<T>::MVector(int s){// проверка sp = new T[sz=s];for (int i = 0; i<s; i++) p[i] = 0;}template<class T> MVector<T>::~MVector(){for (int i = 0; i<s; i++) delete p[i];delete p;}Пользователь может рассчитывать, что содержащиеся в MVector указатели будут удалены. Отсюдаследует, что после удаления MVector пользователь не должен обращаться с помощью указателей кобъектам, заносившимся в этот контейнер.
В момент уничтожения MVector в нем не должно бытьуказателей на статические или автоматические объекты, например:void h()// корректное использование средств управления памятью{MVector<Shape*> v(10);Circle* cp = new circle();v[0] = cp;v[1] = new Triangle;Square s;v[2] = &s;v[2] = 0;// предотвращает удаление s364Бьерн Страуструп.Язык программирования С++// все оставшиеся указатели// автоматически удаляются при выходе}Естественно, такое решение годится только для контейнеров, в которых не содержатся копии объектов,а для класса Map ($$8.8), например, оно не годится. Здесь приведен простой вариант деструктора дляMVector, но содержится ошибка, поскольку один и тот же указатель, дважды занесенный в контейнер,будет удаляться тоже два раза.Построение и уничтожение таких контейнеров, которые следят за удалением содержащихся в нихобъектах, довольно дорогостоящая операция.