Автоматический класс памяти (automatic storage duration)
Статический класс памяти (static storage duration)
Динамический класс памяти (dynamic storage duration)
Потоковый класс памяти (thread storage duration). Не затрагивается в статье.
Эта статья не содержит в себе практической составляющей, а является теоретической нагрузкой с целью расширения кругозора читателя. Я не могу обещать, что всё изложу достаточно просто, но я старался описать так, чтобы новичок смог принять сознанием некоторый образ модели памяти, заложенный в язык С++. Поскольку о классах памяти можно или что-то читать, или что-то слышать и при этом плохо понимать или вообще не понимать, о чём речь, мне захотелось написать эту статью. На самом деле, когда мне говорили, что, например, переменная хранится в стеке, у меня была внутренняя паника: "это как, это что, не понимаю…". И было такое достаточно долго. Когда говорили: "в куче" — такая же внутренняя паника: "что за куча, почему куча, не понимаю…" Но потом я открыл учебник Прата, а там прочитал вступительную статью о моделях памяти и понял некоторые достаточно простые истины.
Независимо от физического исполнения ОЗУ ЭВМ и от организации памяти, предлагаемой операционной системой, с точки зрения С++ память языка С++ всегда описывается одинаково. Это описание называется моделью памяти С++.
Модель памяти в С++ позволяет нам не думать ни о внешнем устройстве памяти, ни об особенностях памяти операционных систем, а даёт возможность думать о памяти как о памяти, описанной самим языком С++. Модель, описанная языком С++, даёт нам три хранилища: автоматическое хранилище, статическое хранилище и динамическое хранилище. Начиная с С++11 даётся ещё одно хранилище: потоковое.
Любая сохраняемая в компьютере информация хранится либо на носителях, т. е. дисках, либо в оперативной памяти. Поскольку доступ к информации, хранимой на дисках, происходит значительно медленнее, чем доступ к непосредственно оперативной памяти компьютера, программы, как правило, используют оперативную память компьютера. Любая запоминаемая информация должна где-то храниться: хранится всё в одном из указанных хранилищ. От способа выделения памяти (т. е. от выбора хранилища) зависит как долго существует запомненное в ячейку хранилища значение.
Переменные могут храниться постоянно и быть востребованы в любой момент времени программы. Такие переменные называют глобальными переменными и статическими переменными. Задействуется статическое хранилище. Говорят, что у переменной статический класс памяти.
Переменные могут зарождаться внутри блока и с завершением блока уничтожаться. Такие переменные могут быть востребованы только внутри своего блока или во вложенных в собственный блок блоках. Такие переменные называют локальными переменными. Говорят, что у переменной автоматический класс памяти.
Переменные могут зарождаться в любом месте программы с помощью операций ручного управления памятью, например, new или специальными функциями,например, malloc. Когда программист берёт управление в свои руки — задействуется динамическое хранилище. Говорят, что у переменной динамический класс памяти.
При программировании многопоточных приложений можно создавать переменные с потоковым классом памяти.
В настоящее время (пока многопоточное программирование в С++ не особо широко развернулось) наиболее широко используются переменные с автоматическим классом хранения.
Локальные переменные, явно объявленные со спецификатором register, либо не объявленные явно со спецификатором static или extern, обладают автоматическим классом памяти (automatic storage duration). Память для этих сущностей сохраняется до тех пор, пока управление не выйдет из того блока, в котором они созданы.
Обозначение автоматические происходит из автоматизированной работы компилятора, который самостоятельно выделяет память ради переменных и самостоятельно зачищает память, когда переменная больше не нужна. На самом деле автоматические переменные являются локальными по отношению к блоку, в котором они объявлены. Блок — это раздел кода, ограниченный фигурными скобками.
Автоматические переменные, как правило, добавляются в память в порядке своего создания, а уничтожаются в направлении от последней созданной к первой. Такая организация добавления и удаления называется стек, поэтому говорят, что переменные хранятся в стеке.
Переменные со статическим классом хранения используются заметно реже, чем остальные. Статический класс хранения переменная получает если она глобальная, либо если к изначально локальной переменной приписать ключевое слово static.
В совокупности отличий глобальных или static-переменных от автоматических локальных — это то, что при компиляции программы под первые выделяется физическая память, которую высчитывает компилятор. Компилятор когда видит первые две, он смотрит на их тип и сразу бронирует им место в памяти. А под автоматические переменные маленький кусочек физической памяти выделяется до запуска программы (в память записываются адреса функций, отводится память под параметры функций и записывается адрес ) память выделяется уже в ходе работы программы. Т. е. компилятор собрал и запустил программу, происходят процессы выделения физической памяти или освобождения физической памяти под локальные переменные. Это значит, что при использовании локальных переменных (которые автоматические) память расходуется эффективнее, что и служит одной из основных причин предпочтения локальных переменных глобальным (либо static-переменным). Другим важным отличием является неудобство использования названий глобальных переменных, существуют большие риски словить конфликт имён.
Главный момент, который вы должны запомнить сейчас относительно автоматического и статического хранилищ — то, что ими строго определяется время жизни переменных. Переменные могут либо существовать на протяжении всего выполнения программы (статические переменные), либо только в период выполнения функции или блока (автоматические переменные).
Некоторого рода промежуточным вариантом хранения оказываются переменные, попадающие в динамическое хранилище. Такие переменные создаются насильным обращением к памяти и пока насильно не будут уничтожены — будут занимать память. Динамическое хранилище — это, можно сказать, свободное хранилище. Переменные, хранимые в нём, наиболее гибкие по отношению к автоматическим и статическим переменным. Но управлять динамическим хранилищем сложнее, чем автоматическим и статическим, которыми управляет компилятор. В С++, чтобы использовать динамическое хранилище, принято использовать пару newdelete, хотя некоторые программисты предпочитают стиль С, используют функции malloc, realloc для выделения, перераспределения и free для очистки соответсвенно.
В стеке механизм автоматического добавления и удаления приводит к тому, что части памяти могут быть смежными. Тем не менее, чередование операций new и delete может привести к появлению промежутков в свободном хранилище, усложняя отслеживание места, где будут распределяться новые запросы памяти.
Динамическое хранилище используют для создания сложных структур данных, начиная от массива, умеющего изменять свой размер, и идя дальше (стеки, очереди, деревья и др. динамические структуры данных). Динамическое хранилище можно задействовать только с помощью указателей.
Программисты часто используют простые обозначения, разделяющие переменные с динамическим классом хранения и со статическим классом хранения, говоря о том, что одни переменные хранятся в куче (динамические переменные), а другие в стеке (автоматические переменные).
Потоковые переменные я не могу расписать, потому что в основной книге, на базе которой написана эта статья, параллельное программирование не рассматривается. А я ещё не дорос.
Использованные материалы:
C++
1
2
3
4
5
6
//clang Листинг #1 Переменные со статическим классом хранения
inta;//Переменная вне блоков функций - имеют статический класс памяти
intmain(){
staticintb;//static-переменные имеют статический класс памяти
}
C++
1
2
3
4
5
6
7
8
//clang Листинг #2 Переменные с автоматическим классом хранения
voidfoo(){
intx;//обычные локальные переменные имеют автоматический класс памяти
}
intmain(){
intx;//обычные локальные переменные имеют автоматический класс памяти
}
C++
1
2
3
4
5
6
7
8
9
10
//clang Листинг #3 Переменные сдинамическим классом хранения
int*foo(){
int*ptr=newint;//Ручное управление памятью
//ptr имеет динамический класс памяти
}
intmain(){
int*p=foo();//Переменные с динамическим классом памяти живут
deletep;//Пока не будут убиты принудительно
}
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//clang Листинг #4 ПРИМЕР ОШИБКИ Хоть код компилируется, результаты могут быть плачевны
#include <iostream>
usingnamespacestd;
int*foo(){
inta=100;//у "a" автоматический класс хранения
int*ptr;//у ptr автоматический класс хранения
ptr=&a;//Ссылаемся на a
returnptr;
}
intmain(){
int*value=foo();//Ошибка! Хотя компилируется
(*value)++;//Попытка использовать ошибочное значение
cout<<*value;//К успеху не приводит
cin.get();
}
Результат четвёртого листинга показывает потенциальную ошибку программиста, не понимающего принципы организации классов хранения. По задумке написавшего этот код, возможно, была попытка вытащить из функции foo адрес закрытой внутри foo переменной a. Зачем это понадобилось, кто его знает, может, очень хотел таким образом сэкономить память, чтобы не выделять дополнительной памяти для новой переменной. В таком случае налицо непонимание того, что память для автоматических переменных выделяется уже после запуска программы. Только когда функция foo будет запущена, только тогда будет выделение в памяти области для хранения значения а, а когда функция прекратит своё существование, непосредственно перед завершением функции произойдут процессы освобождения памяти. Поскольку переменная внутри функции имеет автоматический класс хранения — она уничтожается тогда, когда она уже не нужна. Поскольку она нужна только внутри функции, то вне функции её уже не существует. Таким образом получается, что указатель, который внутри функции, направлен на незадействованный адрес памяти, на мусорное значение.
Вылечить можно несколькими способами: изменить класс памяти переменной, другими. Поскольку статья о классах памяти, то мы будем лечить проблему двумя способами: изменим класс памяти проблемной переменной на статический, а потом на динамический.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//clang Листинг #5.1 Лечим ошибку статическим классом памяти
#include <iostream>
usingnamespacestd;
intfoo(){
staticinta=100;//у "a" статический класс хранения
returna;
}
intmain(){
intvalue=foo();//Теперь всё хорошо
value++;//Используется полезная память
cout<<value;//Теперь всё работает, 101
}
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//clang Листинг #5.2 Лечим ошибку динамическим классом памяти
#include <iostream>
usingnamespacestd;
int*foo(){
int*a=newint(100);//у "a" динамический класс хранения
int*ptr;//у ptr автоматический класс хранения
ptr=a;//Присваиваем указателю значение другого указателя
returnptr;
}
intmain(){
int*value=foo();//Теперь всё хорошо
(*value)++;//Используется полезная память
//*value++;
cout<<*value;//Теперь всё работает
delete value;//Динамическое надо чистить насильно
}
Со статическим классом памяти происходит бронирование адреса памяти во время загрузки программы. Адрес не освобождается, пока работает сама программа. С динамическим классом памяти программа эффективнее использует память, но управлять процессом написания кода становится сложнее. Так, например, показана возможная ошибка, где *value++ сдвигает указатель, а не изменяет хранимое в адресе значение. Другая ошибка возможна с delete, где можно или неправильно написать, чем сделать непрогнозируемое поведение программы, или вообще забыть написать, чем спровоцировать утечку памяти.
Добавить комментарий