Справочник функций

Ваш аккаунт

Войти через: 
Забыли пароль?
Регистрация
Информацию о новых материалах можно получать и без регистрации:

Почтовая рассылка

Подписчиков: -1
Последний выпуск: 19.06.2015

Оперативная память. Эпизод III. Управление памятью в приложениях

Автор: Шаймарданов Булат
8 августа 2006 года

Введение

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

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

В языке C++ (именно на его примере рассмотрим системы, где ответственность за своевременное освобождение блоков памяти лежит на программисте) существуют предусмотренные стандартом C++, а также перешедшие "в наследство" от C средства выделения и освобождения памяти. Кроме того, каждая платформа предоставляет свои средства управления памятью (как правило, стандартные языковые средства как раз к ним и обращаются, но у программиста есть возможность обратиться к ним непосредственно). Проблема этих систем и языков (в том числе C++) состоит в том, что программист сам отвечает за своевременное освобождение блоков памяти после того, как в них отпадает необходимость - для этого тоже есть специальные функции. Если программист забудет освободить блок, впоследствии может возникнуть нехватка памяти - виртуальное адресное пространство процесса "забьется" и свободного места не останется, хотя в то же время в памяти будут присутствовать блоки, которые фактически не используются.

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

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

new Type (parameters)

где Type - тип создаваемого объекта, на основе которого компилятор автоматически определит требуемый для него объем памяти;

parameters - необязательный список параметров, который будет передан конструктору объекта. В случае отсутствия списка параметров скобки указывать необязательно.

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

Функции операционной системы Windows - LocalAlloc и GlobalAlloc - считаются устаревшими, хотя и поддерживаются в целях совместимости. Современным приложениям рекомендуется пользоваться HeapAlloc, а также VirtualAlloc, которая, помимо выделения памяти, поддерживает операцию резервирования памяти и выделения зарезервированной памяти.

Соответственно, средства освобождения памяти следующие.

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

Функция free () освобождает выделенный с помощью malloc () блок. Следует передать адрес блока.

Функции Windows по освобождению памяти называются LocalFree, GlobalFree, HeapFree и VirtualFree.

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

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

Сборщик мусора как средство автоматического контроля памяти

В системах, имеющих "сборщик мусора", программисту не нужно вручную освобождать занятую им память - этим занимается сборщик мусора. Примеров таких систем можно привести множество: .NET, Java, Visual Basic: Следует отметить, что кроме наличия сборки мусора большинство таких языков характеризуются еще и нетипизированностью - объявляя переменную, программист не ограничивает ее тип и может хранить в ней всё что угодно.

Как это происходит? На самом деле всё достаточно просто - достаточно к каждому объекту "привязать" счетчик ссылок, который увеличивается каждый раз, когда на выделенный блок памяти ссылается какая-то переменная, и уменьшается, когда в эту переменную записывается другое значение, т. е. ссылка на объект теряется. К примеру, мы объявили некоторую переменную и тут же создали объект, присвоив ссылку на него этой переменной. Тут же счетчик ссылок этого объекта становится равным 1, т. к. наша переменная ссылается на объект. Теперь мы вызываем некоторую функцию, в качестве одного из параметров которой передаем нашу переменную. Тут счетчик ссылок следует опять увеличить, и он станет равным 2. Естественно, в момент выхода из вызванной функции он вернется в состояние 1. Если мы объявим вторую переменную и присвоим ей значение первой - счетчик вновь увеличится. Как только счетчик ссылок достигает нуля, объект можно удалять, поскольку адрес объекта нигде, ни в какой переменной или блоке памяти не содержится, таким образом, программа "забыла" ее адрес и всё равно никак не сможет прочитать или изменить объект.

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

Кроме очевидного преимущества, у системы сборки мусора есть свои подводные камни. В этих системах невозможно или сложно реализовать "ручные" механизмы выделения и освобождения памяти - к примеру, если на C++ вы можете с легкостью запросить память у операционной системы огромным куском, а внутри этого большого "куска" реализовать свои механизмы выделения/освобождения памяти, перегрузив операторы new и delete, вы теоретически сможете путем ручного управления памятью ускорить работу своего приложения (если, конечно, сможете придумать алгоритм, более оптимальный, чем стандартные), то в таких языках и средах вы этого сделать не сможете. А стандартные методы освобождения и выделения приходится иногда переписывать - например, если вы хотите хранить данные в файле и использовать отображение файлов на память. Другой подводный камень - система сборки мусора может физически переместить объект (естественно, поскольку адрес объекта изменился, все ссылки на него стали недействительными - их надо исправить, и это дело системы сборки мусора). Таким образом, вы не можете "доверять" адресу переменной (если вообще есть возможность его получить), так как он непостоянен. Кроме того, чтобы у сборщика мусора была возможность перемещать объекты физически, объект должен содержать не просто счетчик ссылок, а список самих ссылок, что увеличивает расходы (как расход памяти, так и ресурс времени) на содержание объектов.

Возможности по реализации автоматического управления памятью в C++

Несмотря на отсутствие сборки мусора в C++, его, в общем-то, несложно реализовать самому. Предлагаю один из примеров такой реализации.

1. Опишем базовый класс, который будет прародителем всех объектных классов. Назовем его, скажем, CUniObject. Единственным членом класса будет счетчик ссылок (unsigned int count). Счетчик ссылок будет приватным.

2. Описываем класс универсального указателя CUniPointer и вместо указателей на объекты будем создавать экземпляры этого класса. Для поддержки безопасности типов класс CUniPointer следует объявить шаблонным (например, так: template class CUniPointer<typename basetype>), где в качестве параметра будет использоваться тип, на который мы хотим указывать. Тип, очевидно, будет производным от CUniObject, поэтому класс CUniPointer может смело ссылаться на basetype::count. Собственно указатель будет являться членом этого класса: basetype *pointer.

3. Чтобы класс указателя мог иметь непосредственный доступ к счетчику ссылок своего типа-параметра, класс CUniPointer следует объявить дружественным в каждом производном классе от CUniObject (friend class CUniPointer<ЭтотКласс>).

4. Объявим два конструктора: первый - конструктор по умолчанию - предназначен для создания неинициализированных объектов-указателей (pointer = NULL), второй - для инициализированных. Последний будет принимать параметром ссылку на другой объект. Другая версия инициализирующего конструктора будет принимать параметр-указатель (для того, чтобы мы могли объявить переменную вот так: CUniPointer<SomeType> ptr = new SomeType). Причем конструктор по умолчанию инициализирует pointer значением NULL, счетчик ссылок при этом, конечно, не затрагивается. Остальные конструкторы увеличивают счетчик ссылок объекта, находящегося по адресу pointer, на 1: pointer->count++.

5. В момент присваивания объекту-указателю нового значения счетчики ссылок старого объекта и нового объекта следует также менять. Для этого перегрузим оператор = в классе CUniPointer примерно таким образом:

template <typename basetype> CUniPointer<basetype>
&CUniPointer<basetype>::operator = (basetype *new_ptr)
{
	if (pointer != NULL)
		pointer->count--;
	pointer = new_ptr;
	if (pointer != NULL)
		pointer->count++;
	return *this;
}

Таким образом, если раньше объект-указатель на что-то указывал, счетчик ссылок этого объекта уменьшим, поскольку теперь объект-указатель на него указывать не будет. Напротив, счетчик ссылок нового объекта увеличим, конечно, в том случае, если новый указатель не пуст.

6. Деструктор объекта-указателя должен также уменьшать счетчик ссылок, если pointer не пуст.

7. Остается добавить в оператор = и в деструктор специальный код, проверяющий счетчик ссылок объекта на равенство нулю, и уничтожающий объект в случае равенства. Таким образом, окончательный вид оператора = будет таков:

template <typename basetype> CUniPointer<basetype>
&CUniPointer<basetype>::operator = (basetype *newptr)
{
	if (pointer != NULL)
		if (--(pointer->count) == 0)
			delete pointer;
	pointer = newptr;
	if (pointer != NULL)
		pointer-count++;
	return *this;
}

Дополнительно ко всему этому можно перегрузить оператор = с параметром типа CUniPointer<basetype> и реализовать оператор ->, который упростит доступ к членам объекта pointer, так, что объект CUniPointer будет работать как обычный указатель:

template <typename basetype>
basetype *CUniPointer<basetype>::operator -> ()
{
	return pointer;
}

В результате этих действий мы, по сути, получим эмулятор сборщика мусора.

Замечу, что в стандарте C++ предусмотрен специальный класс - автоуказатель - который несколько похож на описанный выше CUniPointer и самостоятельно уничтожает содержащийся внутри него указатель в своём деструкторе. Это класс template <typename _Type> class autoptr, и находится он в пространстве имен std. Есть очень важное отличие: autoptr не содержит счетчика ссылок и освобождает память в любом случае. Значит, если вы опишете функцию, в которой объявите экземпляр переменной autoptr, и значение указателя, содержащегося в ней, вернёте этой функцией, вы, по сути, вернете указатель на свободную память, не занятую никаким объектом, т. к. при выходе из вашей функции объект autoptr будет уничтожен и вместе с ним будет уничтожен ваш указатель. Поэтому autoptr - это не сборщик мусора, а именно автоматически уничтожаемый указатель. Тем не менее, он находит применения.

Остается только отметить, что и класс autoptr, и CUniPtr обладают хорошим свойством: прекрасно работают в условиях обработки исключений. Другими словами, объект autoptr или CUniPtr автоматически будет уменьшать счетчик ссылок и уничтожать указатель при условии счетчик = 0 даже тогда, когда произойдет исключение.

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

Оставить комментарий

Комментарий:
можно использовать BB-коды
Максимальная длина комментария - 4000 символов.
 

Комментарии

1.
350
01 июня 2006 года
cheburator
589 / / 01.06.2006
+1 / -0
Мне нравитсяМне не нравится
10 мая 2007, 23:35:42
Дополнительная информация для расширения кругозора.
Идея основана на идиоме "RAII" - resource acqisition is initialization. Думаю, ссылки легко найдете сами, а если нет - читайте www.gotw.ca/gotw - очень полезный ресурс по C++. Библиотека boost реализовала "умные указатели" (smart pointers) нескольких видов - см. boost.org.
2.
28K
31 марта 2007 года
vector-21h
0 / / 31.03.2007
+0 / -57
Мне нравитсяМне не нравится
31 марта 2007, 20:06:44
to: id_Zebrikoff "Можни ли с ходу доверять такой статье, если сам автор (спасибо cheburator'у) видно код не запускал..."
А зачем доверять? И зачем код запускать? Здесь же идея описана, а не код который надо копи-пастить.
3.
350
01 июня 2006 года
cheburator
589 / / 01.06.2006
+0 / -57
Мне нравитсяМне не нравится
15 января 2007, 21:06:42
Не за что. Тем более, что cheburator и есть автор статьи...
4.
20K
08 ноября 2006 года
id_Zebrikoff
8 / / 08.11.2006
+0 / -57
Мне нравитсяМне не нравится
8 ноября 2006, 23:15:20
вот блин, вопрос. Можни ли с ходу доверять такой статье, если сам автор (спасибо cheburator'у) видно код не запускал...
5.
350
01 июня 2006 года
cheburator
589 / / 01.06.2006
+0 / -56
Мне нравитсяМне не нравится
9 августа 2006, 13:20:55
Заметил пару ошибок в своей статье.
1. В параграфе "Возможности по реализации автоматического управления памятью в C++" в 3-м абзаце:
template class CUniPointer<typename basetype>
следует читать как
template <typename basetype> class CUniPointer
2. В предпоследнем абзаце статьи:
CUniPtr
следует читать как
CUniPointer
Реклама на сайте | Обмен ссылками | Ссылки | Экспорт (RSS) | Контакты
Добавить статью | Добавить исходник | Добавить хостинг-провайдера | Добавить сайт в каталог