Статическое выделение памяти в c
В традиционных языках программирования, таких как Си , Фортран, Паскаль , существуют три вида памяти: статическая, стековая и динамическая . Конечно, с физической точки зрения никаких различных видов памяти нет: оперативная память - это массив байтов, каждый байт имеет адрес , начиная с нуля. Когда говорится о видах памяти, имеются в виду способы организации работы с ней, включая выделение и освобождение памяти , а также методы доступа.
Статическая память
Статическая память выделяется еще до начала работы программы, на стадии компиляции и сборки. Статические переменные имеют фиксированный адрес, известный до запуска программы и не изменяющийся в процессе ее работы. Статические переменные создаются и инициализируются до входа в функцию main , с которой начинается выполнение программы.
Существует два типа статических переменных:
- глобальные переменные - это переменные, определенные вне функций, в описании которых отсутствует слово static . Обычно описания глобальных переменных, включающие слово extern , выносятся в заголовочные файлы (h-файлы). Слово extern означает, что переменная описывается, но не создается в данной точке программы. Определения глобальных переменных, т.е. описания без слова extern , помещаются в файлы реализации (c-файлы или cpp-файлы). Пример: глобальная переменная maxind описывается дважды:
- в h-файле с помощью строки
- Статическую переменную можно описать и внутри функции, хотя обычно так никто не делает. Переменная размещается не в стеке, а в статической памяти, т.е. ее нельзя использовать при рекурсии, а ее значение сохраняется между различными входами в функцию. Область видимости такой переменной ограничена телом функции, в которой она определена. В остальном она подобна статической или глобальной переменной. Заметим, что ключевое слово static в языке Си используется для двух различных целей:
- как указание типа памяти: переменная располагается в статической памяти, а не в стеке;
- как способ ограничить область видимости переменной рамками одного файла (в случае описания переменной вне функции).
Стековая, или локальная, память
Локальные, или стековые, переменные - это переменные, описанные внутри функции. Память для таких переменных выделяется в аппаратном стеке, см. раздел 2.3.2. Память выделяется в момент входа в функцию или блок и освобождается в момент выхода из функции или блока. При этом захват и освобождение памяти происходят практически мгновенно, т.к. компьютер только изменяет регистр, содержащий адрес вершины стека.
Локальные переменные можно использовать при рекурсии, поскольку при повторном входе в функцию в стеке создается новый набор локальных переменных, а предыдущий набор не разрушается. По этой же причине локальные переменные безопасны при использовании нитей в параллельном программировании (см. раздел 2.6.2). Программисты называют такое свойство функции реентерабельностью, от англ. re-enter able - возможность повторного входа. Это очень важное качество с точки зрения надежности и безопасности программы! Программа, работающая со статическими переменными, этим свойством не обладает, поэтому для защиты статических переменных приходится использовать механизмы синхронизации (см. 2.6.2), а логика программы резко усложняется. Всегда следует избегать использования глобальных и статических переменных, если можно обойтись локальными.
Недостатки локальных переменных являются продолжением их достоинств. Локальные переменные создаются при входе в функцию и исчезают после выхода из нее, поэтому их нельзя использовать в качестве данных, разделяемых между несколькими функциями. К тому же, размер аппаратного стека не бесконечен, стек может в один прекрасный момент переполниться (например, при глубокой рекурсии), что приведет к катастрофическому завершению программы. Поэтому локальные переменные не должны иметь большого размера. В частности, нельзя использовать большие массивы в качестве локальных переменных.
Динамическая память, или куча
Помимо статической и стековой памяти, существует еще практически неограниченный ресурс памяти, которая называется динамическая, или куча ( heap ). Программа может захватывать участки динамической памяти нужного размера. После использования ранее захваченный участок динамической памяти следует освободить.
Под динамическую память отводится пространство виртуальной памяти процесса между статической памятью и стеком. (Механизм виртуальной памяти был рассмотрен в разделе 2.6.) Обычно стек располагается в старших адресах виртуальной памяти и растет в сторону уменьшения адресов (см. раздел 2.3). Программа и константные данные размещаются в младших адресах, выше располагаются статические переменные. Пространство выше статических переменных и ниже стека занимает динамическая память:
Динамическое и статическое выделение памяти. Преимущества и недостатки. Выделение памяти для одиночных переменных операторами new и delete . Возможные критические ситуации при выделении памяти. Инициализация при выделении памяти
Содержание
- 1. Динамическое и статическое (фиксированное) выделение памяти. Главные различия
- 2. Преимущества и недостатки использования динамического и статического способов выделения памяти
- 3. Как выделить память оператором new для одиночной переменной? Общая форма.
- 4. Как освободить память, выделенную под одиночную переменную оператором delete ? Общая форма
- 5. Примеры выделения ( new ) и освобождения ( delete ) памяти для указателей базовых типов
- 6. Что такое «утечка памяти» (memory leak)?
- 7. Каким образом выделить память оператором new с перехватом критической ситуации, при которой память может не выделиться? Исключительная ситуацияbad_alloc . Пример
- 8. Выделение памяти для переменной с одновременной инициализацией. Общая форма. Пример
Поиск на других ресурсах:
1. Динамическое и статическое (фиксированное) выделение памяти. Главные различия
Для работы с массивами информации, программы должны выделять память для этих массивов. Для выделения памяти под массивы переменных используются соответствующие операторы, функции и т.п.. В языке программирования C++ выделяют следующие способы выделения памяти:
1. Статическое (фиксированное) выделение памяти. В этом случае память выделяется только один раз во время компиляции. Размер выделенной памяти есть фиксированным и неизменным до конца выполнения программы. Примером такого выделения может служить объявление массива из 10 целых чисел:
2. Преимущества и недостатки использования динамического и статического способов выделения памяти
Динамическое выделение памяти по сравнению со статическим выделением памяти дает следующие преимущества:
- память выделяется по мере необходимости программным путем;
- нет лишних затрат неиспользованной памяти. Выделяется столько памяти сколько нужно и если нужно;
- можно выделять память для массивов информации, размер которых заведомо неизвестен. Определение размера массива формируется в процессе выполнения программы;
- удобно осуществлять перераспределение памяти. Или другими словами, удобно выделять новый фрагмент для одного и того же массива, если нужно выделить дополнительную память или освободить ненужную;
- при статическом способе выделения памяти трудно перераспределять память для переменной-массива, поскольку она уже выделена фиксировано. В случае динамического способа выделения, это делается просто и удобно.
Преимущества статического способа выделения памяти:
- статическое (фиксированное) выделение памяти лучше использовать, когда размер массива информации заведомо известен и есть неизменным на протяжении выполнения всей программы;
- статическое выделение памяти не требует дополнительных операций освобождения с помощью оператора delete . Отсюда вытекает уменьшение ошибок программирования. Каждому оператору new должен соответствовать свой оператор delete ;
- естественность (натуральность) представления программного кода, который оперирует статическими массивами.
В зависимости от поставленной задачи, программист должен уметь правильно определить, какой способ выделения памяти подходит для той или другой переменной (массива).
3. Как выделить память оператором new для одиночной переменной? Общая форма.
Общая форма выделения памяти для одиночной переменной оператором new имеет следующий вид:
- ptrName – имя переменной (указателя), которая будет указывать на выделенную память;
- type – тип переменной. Размер памяти выделяется достаточный для помещения в нее значения переменной данного типа type .
4. Как освободить память, выделенную под одиночную переменную оператором delete ? Общая форма
Общая форма оператора delete для одиночной переменной:
где ptrName – имя указателя, для которого была раньше выделена память оператором new . После выполнения оператора delete указатель ptrName указывает на произвольный участок памяти, который не является зарезервированным (выделенным).
5. Примеры выделения ( new ) и освобождения ( delete ) памяти для указателей базовых типов
В примерах демонстрируется использование операторов new и delete . Примеры имеют упрощенный вид.
Пример 1. Указатель на тип int . Простейший пример
Пример 2. Указатель на тип double
⇑
7. Каким образом выделить память оператором new с перехватом критической ситуации, при которой память может не выделиться? Исключительная ситуация bad_alloc . Пример
При использовании оператора new возможна ситуация, когда память не выделится. Память может не выделиться в следующих ситуациях:
- если отсутствует свободная память;
- размер свободной памяти меньше чем тот, который был задан в операторе new .
В этом случае генерируется исключительная ситуация bad_alloc . Программа может перехватить эту ситуацию и соответствующим образом обработать ее.
Пример. В примере учитывается ситуация, когда память может не выделиться оператором new . В таком случае осуществляется попытка выделить память. Если попытка удачная, то работа программы продолжается. Если попытка завершилась неудачей, то происходит выход из функции с кодом -1.
8. Выделение памяти для переменной с одновременной инициализацией. Общая форма. Пример
Оператор выделения памяти new для одиночной переменной допускает одновременную инициализацию значением этой переменной.
В общем, выделение памяти для переменной с одновременной инициализацией имеет вид
- ptrName – имя переменной-указателя, для которой выделяется память;
- type – тип на который указывает указатель ptrName ;
- value – значение, которое устанавливается для выделенного участка памяти (значение по указателю).
Пример. Выделение памяти для переменных с одновременной инициализацией. Ниже приводится функция main() для консольного приложения. Продемонстрировано выделение памяти с одновременной инициализацией. Также учитывается ситуация, когда попытка выделить память завершается неудачей (критическая ситуация bad_alloc ).
Кеш, оперативная память, стек и куча, выделение и освобождение памяти
Линейное представление памяти
Адрес Значение (1 байт) 0x0000 . . . 0x1000 1 0x1001 2 0x1002 3 0x1003 4 . . 0xffffffffff . C-cast использовать в С++ нельзя! Как надо приводить типы в С++ и надо ли вообще будет в другой лекции
Знаковые Беззнаковые char unsigned char short unsigned short int unsigned или unsigned int long unsigned long Стандарт не регламентирует размер типов
Размер, бит Тип 8 int8_t, int_fast8_t, int_least8_t 16 int16_t, int_fast16_t, int_least16_t 32 int32_t, int_fast32_t, int_least32_t 64 int64_t, int_fast64_t, int_least64_t Беззнаковая (unsigned) версия - добавление префикса u
Память разбита на сегменты:
- кода (CS)
- данных (DS)
- стека (SS)
Регистр сегмента (CS, DS, SS) указывают на дескриптор.
Для инструкций и стека на смещение в сегменте указывает регистр:
Линейный адрес - это сумма базового адреса сегмента и смещения.
Segment limit (20 bit) - размер сегмента, 55-й бит G определяет гранулярность размера:
- байты, если 0
- страницы, если 1 (размер страницы обычно 4Кб)
- 000 - сегмент данных, только чтение
- 001 - сегмент данных, чтение и запись
- 010 - сегмент стека, только чтение
- 011 - сегмент стека, чтение и запись
- 100 - сегмент кода, только выполнение
- 101- сегмент кода, чтение и выполнение
Кроме задержки (latency) есть понятие пропускной способности (throughput, bandwidth). В случае чтения из RAM - 10-50 Gb/sec
Выводы из таблицы
- Стараться укладывать данные в кеш
- Минимизировать скачки по памяти
- В условиях основной веткой делать ветку которая выполняется с большей вероятностью
Классы управления памятью и областью видимости в C++
Характеризуются тремя понятиями:
Продолжительность хранения данных в памяти
Части кода из которых можно получить доступ к данным
Если к данным можно обратиться из другой единицы трансляции — связывание внешнее (external), иначе связывание внутреннее (internal)
Время жизни Область видимости Связывание Автоматическое (блок) Блок Отсутствует Статический без связывания
Время жизни Область видимости Связывание Статическое Блок Отсутствует Инициализируется при первом обращении
Статический с внутренним связыванием
Время жизни Область видимости Связывание Статическое Файл Внутреннее Инициализируется до входа в main
Статический с внешним связыванием
Время жизни Область видимости Связывание Статическое Файл Внешнее Выделение памяти на стеке очень быстрая, но стек не резиновый
Память в куче выделяют new и malloc, есть сторонние менеджеры памяти.
- new то же, что и malloc, только дополнительно вызывает конструкторы
- Внутри malloc есть буфер, если в буфере есть место, ваш вызов может выполниться быстро
- Если памяти в буфере нет, будет запрошена память у ОС (sbrk, VirtualAlloc) - это дорого
- ОС выделяет память страницами от 4Кб, а может быть и все 2Мб
- Стандартные аллокаторы универсальные, то есть должны быть потокобезопасны, быть одинаково эффективны для блоков разной длины, и 10 байт и 100Мб. Плата за универсальность - быстродействие
Глобальная память (data segment)
Если не удастся разместить блок глобальной памяти, то программа даже не запустится
Значение в квадратных скобках должно быть известно на этапе компиляции, увы
Фактически - это вычисление смещения:
Массив - непрерывный блок байт в памяти, sizeof(data) вернет размер массива в байтах (не элементах!). Размер массива в элементах можно вычислить: sizeof(data) / sizeof(data[0])
Измеряем скорость работы (benchmark)
- Измерений должно быть много
- Одному прогону верить нельзя
- Компилятор оптимизирует, надо ему мешать
- Перед тестами надо греться
Пример "вредной" оптимизации
Не даем компилятору оптимизировать
Написать свой аллокатор со стратегией линейного выделения памяти со следующим интерфейсом:
При вызове makeAllocator аллоцируется указанный размер, после чего при вызове alloc возвращает указатель на блок запрошенного размера или nullptr, если места недостаточно. После вызова reset аллокатор позволяет использовать свою память снова.
Самый простой метод – это объявление переменных внутри функций. Если переменная объявлена внутри функции, каждый раз, когда функция вызывается, под переменную автоматически отводится память. Когда функция завершается, память, занимаемая переменными, освобождается. Такие переменные называют автоматическими .
При создании автоматических переменных они никак не инициализируются, т.е. значение автоматической переменной сразу после ее создания не определено, и нельзя предсказать, каким будет это значение. Соответственно, перед использованием автоматических переменных необходимо либо явно инициализировать их, либо присвоить им какое-либо значение.
Аналогично автоматическим переменным , объявленным внутри функции, автоматические переменные , объявленные внутри блока (последовательности операторов, заключенных в фигурные скобки) создаются при входе в блок и уничтожаются при выходе из блока.
Замечание. Распространенной ошибкой является использование адреса автоматической переменной после выхода из функции. Конструкция типа:
дает непредсказуемый результат.
Статические переменные
Другой способ выделения памяти – статический
Если переменная определена вне функции, память для нее отводится статически, один раз в начале выполнения программы, и переменная уничтожается только тогда, когда выполнение программы завершается. Можно статически выделить память и под переменную, определенную внутри функции или блока. Для этого нужно использовать ключевое слово static в его определении:
В данном примере переменная visited задается в начале выполнения программы. Ее начальное значение – false . При первом вызове функции func условие в операторе if будет истинным, выполнится инициализация, и переменной visited будет присвоено значение true . Поскольку статическая переменная создается только один раз, ее значения между вызовами функции сохраняются. При втором и последующих вызовах функции func инициализация производиться не будет.
Если бы переменная visited не была объявлена static , то инициализация происходила бы при каждом вызове функции.
Динамическое выделение памяти
Третий способ выделения памяти в языке Си++ – динамический . Память для величины какого-либо типа можно выделить, выполнив операцию new . В качестве операнда выступает название типа, а результатом является адрес выделенной памяти.
Созданный таким образом объект существует до тех пор, пока память не будет явно освобождена с помощью операции delete . В качестве операнда delete должен быть задан адрес , возвращенный операцией new :
Динамическое распределение памяти используется, прежде всего, тогда, когда заранее неизвестно, сколько объектов понадобится в программе и понадобятся ли они вообще. С помощью динамического распределения памяти можно гибко управлять временем жизни объектов, например выделить память не в самом начале программы (как для глобальных переменных), но, тем не менее, сохранять нужные данные в этой памяти до конца программы.
Если необходимо динамически создать массив, то нужно использовать немного другую форму new :
В отличие от определения переменной типа массив, размер массива в операции new может быть произвольным, в том числе вычисляемым в ходе выполнения программы. (Напомним, что при объявлении переменной типа массив размер массива должен быть константой.)
Освобождение памяти, выделенной под массив, должно быть выполнено с помощью следующей операции delete
Выделение памяти под строки
В следующем фрагменте программы мы динамически выделяем память под строку переменной длины и копируем туда исходную строку
Операция new возвращает адрес выделенной памяти. Однако нет никаких гарантий, что new обязательно завершится успешно. Объем оперативной памяти ограничен, и может случиться так, что найти еще один участок свободной памяти будет невозможно. В таком случае new возвращает нулевой указатель ( адрес 0). Результат new необходимо проверять:
Рекомендации по использованию указателей и динамического распределения памяти
Указатели и динамическое распределение памяти – очень мощные средства языка. С их помощью можно разрабатывать гибкие и весьма эффективные программы. В частности, одна из областей применения Си++ – системное программирование – практически не могла бы существовать без возможности работы с указателями . Однако возможности, которые получает программист при работе с указателями, накладывают на него и большую ответственность. Наибольшее количество ошибок в программу вносится именно при работе с указателями . Как правило, эти ошибки являются наиболее трудными для обнаружения и исправления.
Приведем несколько примеров.
Использование неверного адреса в операции delete . Результат такой операции непредсказуем. Вполне возможно, что сама операция пройдет успешно, однако внутренняя структура памяти будет испорчена, что приведет либо к ошибке в следующей операции new , либо к порче какой-нибудь информации.
Пропущенное освобождение памяти, т.е. программа многократно выделяет память под данные, но "забывает" ее освобождать. Такие ошибки называют утечками памяти. Во-первых, программа использует ненужную ей память, тем самым понижая производительность. Кроме того, вполне возможно, что в 99 случаях из 100 программа будет успешно выполнена. Однако если потеря памяти окажется слишком большой, программе не хватит памяти под какие-нибудь данные и, соответственно, произойдет сбой.
Запись по неверному адресу . Скорее всего, будут испорчены какие-либо данные. Как проявится такая ошибка – неверным результатом, сбоем программы или иным образом – предсказать трудно
Примеры ошибок можно приводить бесконечно. Общие их черты, обуславливающие сложность обнаружения, это, во-первых, непредсказуемость результата и, во-вторых, проявление не в момент совершения ошибки, а позже, быть может, в том месте программы, которое само по себе не содержит ошибки (неверная операция delete – сбой в последующей операции new , запись по неверному адресу – использование испорченных данных в другой части программы и т.п.).
Отнюдь не призывая отказаться от применения указателей (впрочем, в Си++ это практически невозможно), мы хотим подчеркнуть, что их использование требует внимания и дисциплины. Несколько общих рекомендаций.
- Используйте указатели и динамическое распределение памяти только там, где это действительно необходимо. Проверьте, можно ли выделить память статически или использовать автоматическую переменную .
- Старайтесь локализовать распределение памяти . Если какой-либо метод выделяет память (в особенности под временные данные), он же и должен ее освободить.
- Там, где это возможно, вместо указателей используйте ссылки .
- Проверяйте программы с помощью специальных средств контроля памяти (Purify компании Rational, Bounce Checker компании Nu-Mega и т.д.)
Ссылки
Ссылка – это еще одно имя переменной. Если имеется какая-либо переменная, например
то можно определить ссылку на переменную x как
и тогда x и y обозначают одну и ту же величину. Если выполнены операторы
то y.real равно 1 и y.imaginary равно 2 . Фактически, ссылка – это адрес переменной (поэтому при определении ссылки используется символ & -- знак операции взятия адреса ), и в этом смысле она сходна с указателем , однако у ссылок есть свои особенности.
Во-первых, определяя переменную типа ссылки , ее необходимо инициализировать, указав, на какую переменную она ссылается. Нельзя определить ссылку
Во-вторых, нельзя переопределить ссылку , т.е. изменить на какой объект она ссылается. Если после определения ссылки xref мы выполним присваивание
то выполнится присваивание значения переменной y той переменной, на которую ссылается xref . Ссылка xref по-прежнему будет ссылаться на x . В результате выполнения следующего фрагмента программы:
В-третьих, синтаксически обращение к ссылке аналогично обращению к переменной. Если для обращения к атрибуту объекта, на который ссылается указатель , применяется операция -> , то для подобной же операции со ссылкой применяется точка " .".
Как и указатель , ссылка сама по себе не имеет значения. Ссылка должна на что-то ссылаться, тогда как указатель должен на что-то указывать.
Распределение памяти при передаче аргументов функции
Рассказывая о функциях, мы отметили, что у функций (как и у методов классов) есть аргументы , фактические значения которых передаются при вызове функции.
Рассмотрим более подробно метод Add класса Complex . Изменим его немного, так, чтобы он вместо изменения состояния объекта возвращал результат операции сложения:
При вызове этого метода
значение переменной n2 передается в качестве аргумента . Компилятор создает временную переменную типа Complex , копирует в нее значение n2 и передает эту переменную в метод Add . Такая передача аргумента называется передачей по значению . У передачи аргументов по значению имеется два свойства. Во-первых, эта операция не очень эффективна, особенно если объект сложный и требует большого объема памяти или же если создание объекта сопряжено с выполнением сложных действий (о конструкторах объектов будет рассказано в лекции 12). Во-вторых, изменения аргумента функции не сохраняются. Если бы метод Add был бы определен как
то при вызове n3 = n1.Add(n2) результат был бы, конечно, другой, но значение переменной n2 не изменилось бы. Хотя в данном примере изменяется значение аргумента метода Add , этот аргумент – лишь копия объекта n2 , а не сам объект. По завершении выполнения метода Add его аргументы просто уничтожаются, и первоначальные значения фактических параметров сохраняются.
При возврате результата функции выполняются те же действия, т.е. создается временная переменная, в которую копируется результат, и уже затем значение временной переменной копируется в переменную n3 . Временные переменные потому и называют временными, что компилятор сам создает их на время выполнения метода и сам их уничтожает.
Другим способом передачи аргументов является передача по ссылке . Если изменить описание метода Add на
то при вызове n3 = n1.Add(n2) компилятор будет создавать ссылку на переменную n2 и передавать ее методу Add . В большинстве случаев это намного эффективнее, так как для ссылки требуется немного памяти и создать ее проще. Однако мы получим нежелательный в данном случае эффект. Метод
изменит значение переменной n2 . Операция Add не предусматривает изменения собственных операндов. Чтобы избежать ошибок, лучше записать аргумент с описателем const , который определяет соответствующую переменную как неизменяемую.
В таком случае попытка изменить значение аргумента будет обнаружена на этапе компиляции, и компилятор выдаст ошибку. Передачей аргумента по неконстантной ссылке можно воспользоваться в том случае, когда функция действительно должна изменить свой аргумент . Например, метод Coord класса Figure записывает координаты некой фигуры в свои аргументы :
переменным cx и cy будет присвоено значение координат фигуры fig .
Вернемся к методу Add и попытаемся оптимизировать передачу вычисленного значения. Простое на первый взгляд решение возвращать ссылку на результат не работает:
При выходе из метода автоматическая переменная result уничтожается, и память, выделенная для нее, освобождается. Поэтому результат Add – ссылка на несуществующую память. Результат подобных действий непредсказуем. Иногда программа будет работать как ни в чем не бывало, иногда может произойти сбой, иногда результат будет испорчен. Однако возвращение результата по ссылке возможно, если объект, на который эта ссылка ссылается, не уничтожается после выхода из функции или метода. Если метод Add прибавляет значение аргумента к текущему значению объекта и возвращает новое значение в качестве результата, то его можно записать:
Как и в случае с аргументом , передача ссылки на текущий объект позволяет использовать метод Add слева от операции присваивания, например в следующем выражении:
К значению объекта x прибавляется значение y , а затем результату присваивается значение z (фактически это эквивалентно x = z ). Чтобы запретить подобные конструкции, достаточно добавить описатель const перед типом возвращаемого значения:
Передача аргументов и результата по ссылке аналогична передаче указателя в качестве аргумента:
Если нет особых оснований использовать в качестве аргумента или результата именно указатель , передача по ссылке предпочтительней. Во-первых, проще запись операций, а во-вторых, обращения по ссылке легче контролировать.
Читайте также: