Главная
Новости
Программы
Проекты
Статьи
Ссылки
Контакты
Гостевая
Выпуск №2. Управление памятью

Здравствуйте, уважаемые читатели.

Заранее прошу прощения за оформление этого выпуска, так как время сейчас практически нет, все приходиться делать урывками. Для тех, кто получает рассылку в виде HTML версии, советую принятый html-файл сохранить на диск, а потом его уже оттуда открывать в Internet Explorer.

Управление памятью

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

Я понимаю, что тема работы с памятью очень обширна и в одном выпуске много не напишешь. Для более-менее полной картины пришлось бы написать отдельную книгу, а для нее у меня нет пока столько знаний J . Таким вот образом я решил рассмотреть несколько интересных, на мой взгляд, приемов, которые могли бы облегчить кому-то жизнь.

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

Стековые объекты

Объекты можно разместить в стеке, например:

{

int iDegree;

double dPiValue(3.14159267);

TSomeClass objSomeThing(dPiValue);

}

Таким образом для объектов будет выделена память, потом для каждого объекта будет вызван конструктор. Стековые объекты существуют лишь в границах вмещающего их блока. При выходе за его пределы объект будет уничтожен и автоматически вызовется его деструктор. Для тех, кто не знает, скажу, что в С++ встроенные типы, такие как int и double тоже имеют свой конструктор и деструктор.

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

Динамические объекты

Для создания объекта в куче (heap) можно воспользоваться оператором new, например:

{

int* pDegree = new int;

TSomeClass* pClass = new TSomeClass(pDegree);

}

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

Класс auto_ptr<T>

Кто-то наверно подумал:"Что он тут прописные истины рассказывает?"

Сейчас постараюсь реабилитироваться. J

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

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

Разработчики стандартной библиотеки уже подумали об этом и определили в файле memory маленький шаблон класса-обертки auto_ptr, который можно использовать вместе с любым типом.

Класс auto_ptr содержит указатель на созданный объект и, так называемый, признак владения ( ownership indicator ). Для удобства я бы рекомендовал определять различные специализации шаблона auto_ptr через ключевое слово typedef, например:

typedef auto_ptr<double> TDoublePtr; // авто-указатель на объект типа double

typedef auto_ptr<TClass> TClassPtr; // авто-указатель на объект класса TClass

Вот как выглядит описание класса auto _ ptr в MSDN :

template<class T>

class auto_ptr {

public:

typedef T element_type;

explicit auto_ptr(T *p = 0) throw();

auto_ptr(const auto_ptr<T>& rhs) throw();

auto_ptr<T>& operator=(auto_ptr<T>& rhs) throw();

~auto_ptr();

T& operator*() const throw();

T *operator->() const throw();

T *get() const throw();

T *release() const throw();

};

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

1. Создается экземпляр класса auto _ ptr . Можно создать "пустой" объект, не указывающий ни на что или инициализировать его значением, которое возвращает оператор new или другим объектом класса auto _ ptr . В классе предусмотрен конструктор копий и перегруженный оператор копирования.

2. Объект используется так, как будто это и есть указатель на требуемый тип, либо можно получить "настоящий" указатель с помощью функции get (). При этом один объект класса auto _ ptr можно присваивать другому. На самом деле значение внутреннего указателя на пользовательский тип просто копируется, в объекте-источнике признак владения сбрасывается, а в объекте-приемнике признак владения устанавливается. Для "ручного" управления признаком владения существует функция класса auto _ ptr release . Эта функция устанавливает признак владения в false . В этом случае при выходе из области видимости объект класса auto _ ptr не будет пытаться вызвать оператор delete для объекта , на который указывает его внутренний указатель.

3. При выходе из области видимости объекта auto _ ptr , автоматически вызывается оператор delete для внутреннего указателя. При этом проверяется, указывает ли этот указатель на что-либо и "владеет" ли он им.

Несколько примеров использования auto _ ptr :

//------------------------------------------------

#include "stdafx.h"

#include <memory>

// Включает пространство имен std в глобальную область видимости

using namespace std ;

.// Простенький класс для издевательств

class TClass

{

public:

TClass()

{

::MessageBox(0, "Constructor", 0, MB_OK);

};

~TClass()

{

::MessageBox(0, "Destructor", 0, MB_OK);

};

};

int APIENTRY WinMain(HINSTANCE hInstance,

HINSTANCE hPrevInstance,

LPSTR lpCmdLine,

int nCmdShow)

{

// Создание буфера для строки и резервирование под него 10000 символов

auto_ptr<TCHAR> pString1(new TCHAR[10000]);

// Заполнение буфера нулями. Для доступа к настоящему

// указателю используется функция get ()

:: memset ( pString 1. get (), 0, 10000);

// Создание неинициализированного указателя на тип TCHAR

auto _ ptr < TCHAR > pString 2;

// Присваивание, оно выполняется при помощи перегруженного оператора класса

// auto _ ptr . При копировании копируется значение указателя, а признак владения

// устанавливается для pString 2 и сбрасывается для pString 1. Таким образом гарантируется, что оператор delete будет вызван только один раз для указателя на область памяти, выделенную в первой строке.

pString 2 = pString 1;

// Создание объекта пользовательского класса и присваивание указателя на

// него указателю pClass 1. ПРи этом вызывается конструктор.

auto _ ptr < TClass > pClass 1 ( new TClass );

// Создание указателя на тип TClass . Фактически объект класса TClass не

// создается, а такой указатель ни на что не указывает (внутренний указатель

// равен 0) и признак владения установлен в false .

auto _ ptr < TClass > pClass 2;

// Создание нового объекта TClass и присваивание указателя на него pClass 1.

// Так как pClass 1 уже содержит указатель на существующий объект, то

// сначала этот объект будет удален (вызовется деструктор), а потом pClass 1

// будет присвоен указатель на вновь созданный объект.

pClass1 = auto_ptr<TClass> (new TClass);

// Значение внутреннего указателя pClass 1 присваивается pClass 2. Признак

// владения сбрасывается для pClass 1 и устанавливается для pClass 2.

pClass 2 = pClass 1;

return 0;

// При завершении программы pString 2 и pClass 2 вызовут оператор delete

// для внутренних указателей. Удаление pClass 2 вызовет вызов деструктора.

}

//-------------------------------------------------

Размещение с помощью оператора new

В книге [1] такой "хитрый" синтаксис оператора new, как в классах CSimpleArray и CSimpleMap, имеет свое название: "синтаксис размещения". Определение выглядит в общем виде так:

class TSomeClass

{

//…

public:

void* operator new(size_t, void* p)

{

return p;

}

};

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

В книге [2] автор пишет про методику, которая называется "Конструирование с разделением фаз". Она как раз использует вышеописанный синтаксис. Конструирование с помощью стандартного оператора new выполняется в два шага, выделение памяти и вызов конструктора. Иногда бывает нужно самому выделить память под объект и уже потом в ней разместить его. В этом случае также используется размещение с помощью перегруженного оператора new, например:

void* buffer = ::operator new(1000);

TSomeClass* pClass = new (buffer) TSomeClass;

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

pClass->~TSomeClass;

delete buffer;

Это уже "Уничтожение с разделением фаз". То есть фактически объект уничтожается, но выделенный участок памяти не освобождается, как это происходит при использовании стандартного оператора delete.

Такая вот методика и используется в работе классов CSimpleArray и CSimpleMap библиотеки ATL. В этих классах память выделяется одним большим блоком, в котором в "нужном" месте вставляются элементы. При удалении элементов их деструкторы вызываются напрямую. Участок же памяти, выделенный под весь массив, удаляется при уничтожении всего массива функцией free(void *memblock);

Класс CAutoWinHeap < T >

Мне частенько приходится в программах создавать и использовать различные "буферы", то есть выделять и освобождать какие-то участки памяти….. Для своих целей я набросал небольшой шаблон класса CAutoWinHeap < T >. Основное назначение класса – управление "кусками" динамической памяти. В чем-то этот класс похож на auto _ ptr , но мне его удобнее использовать именно под всякого рода массивы, хранение возвращаемых значений функций и т.д. Тип Т можно использовать любой, даже void . Найти этот класс можно здесь (900 байт, zip -архив).

Описание методов класса CAutoWinHeap < T >
CAutoWinHeap() Конструктор по умолчанию. Создает "пустой" буфер.
CAutoWinHeap(DWORD dwSize) Конструктор создает буфер, размер которого равен dwSize байт.
CAutoWinHeap( CAutoWinHeap<Type>& theHeap) Конструктор копий. Фактически создается второй объект, а данные из участка памяти источника копируются в участок памяти приемника.
virtual ~CAutoWinHeap() Деструктор, освобождает выделенную память.
DWORD GetSize() const Возвращает размер выделенной памяти в байтах.
CAutoWinHeap<Type>& operator =(const CAutoWinHeap<Type>& theHeap) Оператор копирования. Уничтожает данные в участке памяти приемника, перераспределяет ее до размера выделенного участка памяти источника и копирует данные из памяти источника.
operator Type*() const Возвращает указатель типа Т на участок выделенной памяти.
operator bool() const Оператор для использования в конструкциях вида if ( Object ) {…}. Возвращает true , если есть выделенная память и false в противном случае.
bool IsValid () const Возвращает true , если есть выделенная память и false в противном случае.
bool AllocBytes(DWORD size) Выделяет size байт из кучи процесса.
bool AllocObjects(DWORD num = 1) Выделяет количество байт памяти, равной num * sizeof ( T ).
void Free() Освобождает выделенную память и возвращает ее системе.
void ResetMemory() Заполняет нулями выделенную память.

Класс очень простой, вы можете его переписать под свои нужды или создать свой, если захотите.

Память, разделяемая процессами

Иногда требуется создать участок памяти, который будет доступен разным процессам (или проще говоря разным запущенным программам). Есть несколько техник для реализации этого. Здесь я рассмотрю опять же свой небольшой класс CAxSharedMem . Класс определен в пространстве имен AxWinLib . То есть для использования класса нужно либо включить его в глобальную область видимости, либо использовать квалификатор AxWinLib :: для вызова методов класса. Лежит класс здесь (1.4 кБ, zip-архив).

Описание класса CAxSharedMem
CAxSharedMem() Конструктор по умолчанию. Инициализирует переменные класса.
~CAxSharedMem() Деструктор, освобождает занятую память. Пока последний процесс не освободит занятый участок памяти, она будет удерживаться системой.
bool Create(LPCTSTR szName, DWORD dwSize) Создает разделяемый участок памяти с именем szName размером dwSize .
bool Free() Освобождает занятую память. Пока последний процесс не освободит занятый участок памяти, она будет удерживаться системой.
PVOID GetPointer() const Возвращает указатель на разделяемый участок памяти.
DWORD GetSize() const Возвращает размер участка памяти в байтах.
operator PVOID() Перегруженный оператор возвращает указатель на разделяемый участок памяти.
bool IsExists() const После вызова Create , вызов IsExists () возвращает true , если участок памяти уже был создан другим процессом и false в противном случае.

Пример использования класса:

В одной программе:

AxWinLib::CAxSharedMem shared_mem;

shared_mem.Create("Shared memory", 100);

В другой программе :

AxWinLib::CAxSharedMem shared_mem;

shared_mem.Create("Shared memory", 100);

В каждой из программ можно будет использовать участок памяти, указатель на который можно получить с помощью GetPoinetr () или operator PVOID (). Этот участок будет "общим" для обеих программ. То есть если одна из программ запишет что-то в разделяемый участок памяти, то другая сможет его оттуда прочитать и наоборот. Одно важное замечание: при создании нескольких объектов с одинаковым именем, фактически объект создается с указанным размером требуемого участка памяти только при первом вызове метода Create . При последующих вызовах происходит только подключение к созданному участку памяти, при этом размер памяти, указываемый при создании, игнорируется.

Используемая в этом классе функция CreateFileMapping позволяет разграничить доступ к разделяемой памяти, например задать доступ "только для чтения". Я не стал это реализовывать, так как для моих целей это пока не нужно.

Заключение

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

Всего наилучшего.

Литература

  1. Язык программирования С++, Бьерн Страуструп, третье издание, 1999
  2. Библиотека программиста. С++. Джефф Элджер, Питер, 1999
Используются технологии uCoz