Какой объем памяти выделяется под объект
Статическое выделение памяти выполняется для статических и глобальных переменных. Память выделяется один раз (при запуске программы) и сохраняется на протяжении работы всей программы.
Автоматическое выделение памяти выполняется для параметров функции и локальных переменных. Память выделяется при входе в блок, в котором находятся эти переменные, и удаляется при выходе из него.
Динамическое выделение памяти является темой этого урока.
Динамическое выделение переменных
Как статическое, так и автоматическое распределение памяти имеют два общих свойства:
Размер переменной/массива должен быть известен во время компиляции.
Выделение и освобождение памяти происходит автоматически (когда переменная создается/уничтожается).
В большинстве случаев с этим всё ОК. Однако, когда дело доходит до работы с пользовательским вводом, то эти ограничения могут привести к проблемам.
Например, при использовании строки для хранения имени пользователя, мы не знаем наперед насколько длинным оно будет, пока пользователь его не введет. Или нам нужно создать игру с непостоянным количеством монстров (во время игры одни монстры умирают, другие появляются, пытаясь, таким образом, убить игрока).
Если нам нужно объявить размер всех переменных во время компиляции, то самое лучшее, что мы можем сделать — это попытаться угадать их максимальный размер, надеясь, что этого будет достаточно:
char name [ 30 ] ; // будем надеяться, что пользователь введет имя длиной менее 30 символов! Polygon rendering [ 40000 ] ; // этому 3D-рендерингу лучше состоять из менее чем 40000 полигонов!Это плохое решение, по крайней мере, по трем причинам:
Во-первых, теряется память, если переменные фактически не используются или используются, но не все. Например, если мы выделим 30 символов для каждого имени, но имена в среднем будут занимать по 15 символов, то потребление памяти получится в два раза больше, чем нам нужно на самом деле. Или рассмотрим массив rendering : если он использует только 20 000 полигонов, то память для других 20 000 полигонов фактически тратится впустую (т.е. не используется)!
В Visual Studio это можно проверить, запустив следующий фрагмент кода:
int array [ 1000000000 ] ; // выделяем 1 миллиард целочисленных значенийЛимит в 1МБ памяти может быть проблематичным для многих программ, особенно где используется графика.
Для динамического выделения памяти одной переменной используется оператор new:
new int ; // динамически выделяем целочисленную переменную и сразу же отбрасываем результат (так как нигде его не сохраняем)В примере, приведенном выше, мы запрашиваем выделение памяти для целочисленной переменной из операционной системы. Оператор new возвращает указатель, содержащий адрес выделенной памяти.
Для доступа к выделенной памяти создается указатель:
int * ptr = new int ; // динамически выделяем целочисленную переменную и присваиваем её адрес ptr, чтобы затем иметь доступ к нейЗатем мы можем разыменовать указатель для получения значения:
* ptr = 8 ; // присваиваем значение 8 только что выделенной памятиВот один из случаев, когда указатели полезны. Без указателя с адресом на только что выделенную память у нас не было бы способа получить доступ к ней.
Как работает динамическое выделение памяти?
На вашем компьютере имеется память (возможно, большая её часть), которая доступна для использования программами. При запуске программы ваша операционная система загружает эту программу в некоторую часть этой памяти. И эта память, используемая вашей программой, разделена на несколько частей, каждая из которых выполняет определенную задачу. Одна часть содержит ваш код, другая используется для выполнения обычных операций (отслеживание вызываемых функций, создание и уничтожение глобальных и локальных переменных и т.д.). Мы поговорим об этом чуть позже. Тем не менее, большая часть доступной памяти компьютера просто находится в ожидании запросов на выделение от программ.
Когда вы динамически выделяете память, то вы просите операционную систему зарезервировать часть этой памяти для использования вашей программой. Если ОС может выполнить этот запрос, то возвращается адрес этой памяти обратно в вашу программу. С этого момента и в дальнейшем ваша программа сможет использовать эту память, как только пожелает. Когда вы уже выполнили с этой памятью всё, что было необходимо, то её нужно вернуть обратно в операционную систему, для распределения между другими запросами.
В отличие от статического или автоматического выделения памяти, программа самостоятельно отвечает за запрос и обратный возврат динамически выделенной памяти.
Освобождение памяти
Когда вы динамически выделяете переменную, то вы также можете её инициализировать посредством прямой инициализации или uniform-инициализации (в С++11):
int * ptr1 = new int ( 7 ) ; // используем прямую инициализацию int * ptr2 = new int < 8 >; // используем uniform-инициализациюКогда уже всё, что требовалось, выполнено с динамически выделенной переменной — нужно явно указать для С++ освободить эту память. Для переменных это выполняется с помощью оператора delete:
// Предположим, что ptr ранее уже был выделен с помощью оператора new delete ptr ; // возвращаем память, на которую указывал ptr, обратно в операционную систему ptr = 0 ; // делаем ptr нулевым указателем (используйте nullptr вместо 0 в C++11)Оператор delete на самом деле ничего не удаляет. Он просто возвращает память, которая была выделена ранее, обратно в операционную систему. Затем операционная система может переназначить эту память другому приложению (или этому же снова).
Хотя может показаться, что мы удаляем переменную, но это не так! Переменная-указатель по-прежнему имеет ту же область видимости, что и раньше, и ей можно присвоить новое значение, как и любой другой переменной.
Обратите внимание, удаление указателя, не указывающего на динамически выделенную память, может привести к проблемам.
Висячие указатели
Язык C++ не предоставляет никаких гарантий относительно того, что произойдет с содержимым освобожденной памяти или со значением удаляемого указателя. В большинстве случаев, память, возвращаемая операционной системе, будет содержать те же значения, которые были у нее до освобождения, а указатель так и останется указывать на только что освобожденную (удаленную) память.
Указатель, указывающий на освобожденную память, называется висячим указателем. Разыменование или удаление висячего указателя приведет к неожиданным результатам. Рассмотрим следующую программу:
int * ptr = new int ; // динамически выделяем целочисленную переменную * ptr = 8 ; // помещаем значение в выделенную ячейку памяти delete ptr ; // возвращаем память обратно в операционную систему, ptr теперь является висячим указателем std :: cout << * ptr ; // разыменование висячего указателя приведет к неожиданным результатам delete ptr ; // попытка освободить память снова приведет к неожиданным результатам такжеВ программе, приведенной выше, значение 8 , которое ранее было присвоено динамической переменной, после освобождения может и далее находиться там, а может и нет. Также возможно, что освобожденная память уже могла быть выделена другому приложению (или для собственного использования операционной системы), и попытка доступа к ней приведет к тому, что операционная система автоматически прекратит выполнение вашей программы.
Процесс освобождения памяти может также привести и к созданию нескольких висячих указателей. Рассмотрим следующий пример:
int * ptr = new int ; // динамически выделяем целочисленную переменную int * otherPtr = ptr ; // otherPtr теперь указывает на ту же самую выделенную память, что и ptr delete ptr ; // возвращаем память обратно в операционную систему. ptr и otherPtr теперь висячие указатели // Однако, otherPtr по-прежнему является висячим указателем!Есть несколько рекомендаций, которые могут здесь помочь:
Во-первых, старайтесь избегать ситуаций, когда несколько указателей указывают на одну и ту же часть выделенной памяти. Если это невозможно, то выясните, какой указатель из всех «владеет» памятью (и отвечает за её удаление), а какие указатели просто получают доступ к ней.
Правило: Присваивайте удаленным указателям значение 0 (или nullptr в C++11), если они не выходят из области видимости сразу же после удаления.
Оператор new
При запросе памяти из операционной системы в редких случаях она может быть не выделена (т.е. её может и не быть в наличии).
По умолчанию, если оператор new не сработал, память не выделилась, то генерируется исключение bad_alloc . Если это исключение будет неправильно обработано (а именно так и будет, поскольку мы еще не рассматривали исключения и их обработку), то программа просто прекратит свое выполнение (произойдет сбой) с ошибкой необработанного исключения.
Во многих случаях процесс генерации исключения оператором new (как и сбой программы) нежелателен, поэтому есть альтернативная форма оператора new, которая возвращает нулевой указатель, если память не может быть выделена. Нужно просто добавить константу std::nothrow между ключевым словом new и типом данных:
int * value = new ( std :: nothrow ) int ; // указатель value станет нулевым, если динамическое выделение целочисленной переменной не выполнитсяВ примере, приведенном выше, если оператор new не возвратит указатель с динамически выделенной памятью, то возвратится нулевой указатель.
Разыменовывать его также не рекомендуется, так как это приведет к неожиданным результатам (скорее всего, к сбою в программе). Поэтому наилучшей практикой является проверка всех запросов на выделение памяти для обеспечения того, что эти запросы будут выполнены успешно и память выделится:
int * value = new ( std :: nothrow ) int ; // запрос на выделение динамической памяти для целочисленного значения if ( ! value ) // обрабатываем случай, когда new возвращает null (т.е. память не выделяется)Поскольку не выделение памяти оператором new происходит крайне редко, то обычно программисты забывают выполнять эту проверку!
Нулевые указатели и динамическое выделение памяти
Нулевые указатели (указатели со значением 0 или nullptr ) особенно полезны в процессе динамического выделения памяти. Их наличие как бы сообщаем нам: «Этому указателю не выделено никакой памяти». А это, в свою очередь, можно использовать для выполнения условного выделения памяти:
// Если для ptr до сих пор не выделено памяти, то выделяем еёУдаление нулевого указателя ни на что не влияет. Таким образом, в следующем нет необходимости:
Вместо этого вы можете просто написать:
Если ptr не является нулевым, то динамически выделенная переменная будет удалена. Если значением указателя является нуль, то ничего не произойдет.
Утечка памяти
Динамически выделенная память не имеет области видимости, т.е. она остается выделенной до тех пор, пока не будет явно освобождена или пока ваша программа не завершит свое выполнение (и операционная система очистит все буфера памяти самостоятельно). Однако указатели, используемые для хранения динамически выделенных адресов памяти, следуют правилам области видимости обычных переменных. Это несоответствие может вызвать интересное поведение, например:
Здесь мы динамически выделяем целочисленную переменную, но никогда не освобождаем память через использование оператора delete. Поскольку указатели следуют всем тем же правилам, что и обычные переменные, то, когда функция завершит свое выполнение, ptr выйдет из области видимости. Поскольку ptr — это единственная переменная, хранящая адрес динамически выделенной целочисленной переменной, то, когда ptr уничтожится, больше не останется указателей на динамически выделенную память. Это означает, что программа «потеряет» адрес динамически выделенной памяти. И в результате эту динамически выделенную целочисленную переменную нельзя будет удалить.
Это называется утечкой памяти. Утечка памяти происходит, когда ваша программа теряет адрес некоторой динамически выделенной части памяти (например, переменной или массива), прежде чем вернуть её обратно в операционную систему. Когда это происходит, то программа уже не может удалить эту динамически выделенную память, поскольку больше не знает, где выделенная память находится. Операционная система также не может использовать эту память, поскольку считается, что она по-прежнему используется вашей программой.
Хотя утечка памяти может возникнуть и из-за того, что указатель выходит из области видимости, возможны и другие способы, которые могут привести к утечкам памяти. Например, если указателю, хранящему адрес динамически выделенной памяти, присвоить другое значение:
Когда мы создаем переменную int var = 5 , все понятно, компьютер берет (выделяет) память 32 бита и записывает туда значение 5 в двоичном виде.
Но что происходит когда мы создаем переменную типа класс? class a = 5 . Что происходит? Сколько байт выделяется под эту переменную?
11.8k 2 2 золотых знака 13 13 серебряных знаков 26 26 бронзовых знаковОбъект в стеке
В программе выделяется столько байт, сколько требуется для хранения данных экземпляра данного класса. Например, объект пустого класс займёт один байт, если в нём хранится int , то его размер прибавится к размеру объекта класса. Вот интересный код для исследования этих свойств:
Результат выполнения с моим компилятором:
Меняя число переменных можно заметить, что поля с модификатором static не влияют на размер выделенной памяти, что и логично, ведь она не относится к конкретным экземплярам класса.
Объект в куче
При динамическом создании объекта, то есть, выделении памяти в куче, помимо размера самого экземпляра класса можно учесть также и размер указателя на него:
Получается даже, что размер указателя может быть больше самого объекта:
В первом случае - int var = 5; - компилятор выделяет sizeof(int) байтов памяти под переменную var . Это совсем не обязательно 32 бита.
Во втором случае - Class a = 5; - компилятор точно таким же образом выделяет sizeof(Class) байтов под переменную a . Все совершенно единообразно.
Выделение памяти в таких примерах никоим образом не зависит правой части данного объявления, т.е. = 5 никак не влияет на размер выделяемой памяти.
А затем, когда память уже выделена, значение 5 используется в качестве инициализатора для нового объекта. Как именно оно используется - зависит от конкретного типа. В первом случае оно просто заносится в переменную var . А что произойдет во втором случае уже зависит от деталей типа Class . Инициализация в С++ - процесс, описываемый целым набором весьма запутанных правил.
Я не уверен насчет footprint (), так как слышал, что компиляторы могут добавлять дополнительную информацию только для хранения переменных-членов, и я не уверен насчет footprint (), поскольку он ссылается на объект класса. Требует ли это также памяти?
РЕДАКТИРОВАТЬ: Хорошо, скажем, ситуация изменилась так, что мы используем не статический массив, а фактический указатель:
Не только return sizeof(float) * 16 + sizeof(int); довольно громоздкий для более сложного объекта, но он также может быть неверным. Спецификация языка C позволяет компилятору «дополнять» структуры. Скажем, например, у нас есть
Ваш метод определения размера структуры сказал бы, что он занимает 4 + 1 + 8 (или что-то подобное), но фактический sizeof(X) даст 4 + 4 + 8. С экстремальным смешением малых и больших типов данных мы можем легко увидеть ошибку 50-75%. [Точное расположение элементов зависит от компилятора, архитектуры процессора, переключателей компилятора и т. Д.].
Я еще не видел ни одной упомянутой проблемы. Когда вы делаете
когда происходит удаление, ему нужно знать, какой размер был во время new (), чтобы он знал, сколько раз вызывать деструктор std :: string. C ++ может иногда добавлять несколько байтов в выделение, чтобы отслеживать, сколько членов массива вы выделили.
В свете вышеприведенных комментариев, я думаю, необходим еще один ответ.
Я могу придумать несколько довольно хороших решений для ограничения объема памяти, используемой кешем:
1. Начните день, выделите X КБ памяти (независимо от того, что ваш кеш «разрешен» использовать). Разрежьте это на разделы по мере необходимости. Когда полный, возьмите самый старый и используйте его снова (может потребоваться больше чем один, если они не все одинакового размера).
Вам нужен какой-нибудь способ пометить что-то «недавно использованное» или «старое», чтобы вы могли выбросить самые старые вещи. Возможно, с помощью Splay Tree который автоматически переупорядочивает, когда вы выбираете что-то, так что самые последние объекты находятся в верхней части дерева.
После запуска средства можно увидеть пути выполнения функций, для которых выделяются объекты. Затем можно проследить путь к корню дерева вызовов, который использует больше всего памяти.
Установка
Откройте профилировщик производительности (ALT+F2) в Visual Studio.
После его запуска просмотрите сценарий, который вы хотите профилировать в приложении. Затем выберите Остановить сбор или закройте приложение, чтобы просмотреть данные.
Щелкните вкладку Выделение. Теперь содержимое окна должно выглядеть как на снимке экрана ниже.
Теперь вы можете проанализировать выделение памяти объектам.
Во время сбора средство отслеживания может замедлить работу профилированного приложения. Если производительность средства отслеживания или приложения замедлена и если не требуется отслеживать каждый объект, можно настроить частоту выборки. Для этого на странице сводки профилировщика рядом со средством отслеживания щелкните значок шестеренки.
Настройте необходимую частоту выборки. Такое изменение улучшит производительность приложения во время сбора и анализа.
Дополнительные сведения о том, как сделать средство более эффективным, см. в статье Оптимизация параметров профилировщика.
Анализ данных
В предыдущем графическом представлении на верхнем графе показано количество активных объектов в приложении. На нижнем графе Object delta (Изменение объекта) показано процентное изменение объектов приложения. Красные столбцы обозначают, что была проведена сборка мусора.
Можно отфильтровать табличные данные, чтобы отображать действия только для указанного диапазона времени. Можно также увеличивать или уменьшать граф.
Выделение
В представлении Выделение показано местонахождение объектов, которым выделена память, и объем выделенной им памяти.
Столбец Тип содержит список классов и структур, занимающих память. Дважды щелкните тип, чтобы просмотреть его обратную трассировку в виде инвертированного дерева вызовов. Только в представлении Выделение можно просмотреть элементы в выбранной категории, занимающей память.
В столбце Выделения показано количество занимающих память объектов, относящихся к определенному типу выделения или функции. Этот столбец отображается только в представлениях Выделение, Дерево вызовов и Функции.
Столбцы Байты и Средний размер (байты) по умолчанию скрыты. Чтобы их отобразить, щелкните правой кнопкой мыши столбец Тип или Выделения, а затем выберите параметры Байты и Средний размер (байты) , чтобы добавить их в диаграмму.
Эти два столбца похожи на Всего (выделения) и Собственные (выделения) за тем исключением, что вместо количества объектов, занимающих память, в них показан общий объем занимаемой памяти. Эти столбцы отображаются только в представлении Выделение.
В столбце Имя модуля показан модуль, содержащий вызывающую функцию или процесс.
Все эти столбцы можно сортировать. Для столбцов Тип и Имя модуля можно сортировать элементы в алфавитном порядке по возрастанию или убыванию. Для столбцов Выделения, Байты и Средний размер (байт) можно сортировать элементы по увеличению или уменьшению числовых значений.
Символы
На вкладках Выделение, Дерево вызовов и Функции отображаются следующие символы:
— тип значения, например целое число.
— коллекция типа значения, например массив целых чисел.
— ссылочный тип, например строка.
— коллекция ссылочного типа, например массив строк.
Дерево вызовов
В представлении Дерево вызовов отображаются пути выполнения функций, содержащие объекты, для которых выделяется много памяти.
В столбце Имя функции показан процесс или имя функции, содержащей объекты, для которых выделяется память. Отображение зависит от уровня проверяемого узла.
В столбцах Всего (выделения) и Общий размер (байты) отображается количество выделенных объектов и объем памяти, используемые функцией и всеми другими функциями, которые она вызывает.
В столбцах Self (Allocations) (Собственные (выделения)) и Self-Size (Bytes) (Собственный размер (байт)) отображается количество выделенных объектов и объем памяти, используемый одной выбранной функцией или типом выделения.
В столбце Average Size (Bytes) (Средний размер (байт)) отображаются те же сведения, что и в представлении Выделения.
В столбце Имя модуля показан модуль, содержащий вызывающую функцию или процесс.
При нажатии кнопки Развернуть критический путь высвечивается путь выполнения функции, содержащий множество объектов, для которых выделяется память. Алгоритм высвечивает путь с наибольшим количеством выделений, начиная с выбранного узла, помогая изучать использование памяти.
Кнопка Показать критический путь отображает или скрывает значки пламени, указывающие, какие узлы входят в критический путь.
Функции
В представлении Функции показаны процессы, модули и функции, для которых выделяется память.
В столбце Имя отображаются процессы в качестве узлов верхнего уровня. Под процессами располагаются модули, а под ними — функции.
В этих столбцах приводятся те же сведения, что и в представлениях Выделение и Дерево вызовов:
- Total (Allocations) (Всего (выделения));
- Self (Allocations) (Собственные (выделения));
- Общий размер (байт) ;
- Self-Size (Bytes) (Собственный размер (байт));
- Average Size (Bytes) (Средний размер (байт)).
Collection
В представлении Коллекция показано, сколько объектов было собрано или осталось во время сборки мусора. В нем также имеются круговые диаграммы для визуализации собранных и сохранившихся объектов по типу.
- В столбце Собрано показано количество объектов, собранных сборщиком мусора.
- В столбце Осталось показано количество объектов, сохранившихся после работы сборщика мусора.
Средства фильтрации
В каждом из представлений Выделения, Дерево вызовов и Функции есть параметры Показать только мой код и Show Native Code (Показать машинный код), а также поле фильтра.
Читайте также: