Скачиваний:
2
Добавлен:
03.01.2024
Размер:
1.96 Mб
Скачать

СПбГУТ им. проф. М.А. Бонч–Бруевича Кафедра программной инженерии и вычислительной техники (ПИ и ВТ)

ПРОГРАММИРОВАНИЕ

Единственный способ изучать новый язык программирования – писать на нем программы.

Брайэн Керниган

Лекция 12: Работа с динамической памятью

1.Динамическая память

2.Функции для динамического распределения памяти

3.Использование функций malloc и free

4.Одномерные динамические массивы

5.Динамическое выделение памяти для двумерных массивов.

Санкт–Петербург, 2021г.

Введение. Виртуальное адресное пространство процесса в 32 разрядном режиме

 

 

Адресное

 

 

 

Границу между

 

 

 

пространство в

 

 

пользовательским и

 

 

 

32 разрядном

 

 

 

системным

 

 

 

режиме

 

 

диапазоном можно

 

 

 

 

 

 

 

сдвинуть

 

 

 

 

 

 

 

 

 

 

 

 

 

0

 

0

 

 

 

Код и

 

 

 

 

 

 

 

 

 

 

 

 

 

данные

 

 

 

 

 

 

 

 

прикладных

 

 

 

 

 

 

 

 

программ

 

 

 

 

 

 

2Гб

 

user

 

 

 

 

 

 

 

 

 

 

 

 

Код и

kernel OS

 

 

 

 

 

 

 

 

 

 

 

 

 

 

данные

 

 

 

 

user

 

 

ядра,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

память

 

 

 

 

kernel OS

 

 

устройств

 

 

 

 

 

 

4Гб

 

 

4Гб

 

 

 

 

 

 

 

 

 

 

 

 

Взаимодействие между

 

кодом пользовательского

 

режима и ядром часто

изображают так:

 

 

 

Модули и объекты прикладной программы

 

 

 

 

 

 

user

 

 

 

 

 

 

 

 

 

 

 

 

 

 

kernel OS

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Модули и

 

 

 

 

 

 

 

объекты

 

 

 

 

 

 

 

 

 

 

 

 

 

ядра ОС

 

 

 

 

 

 

 

Каждый процесс в многозадачной ОС выполняется в собственной области памяти. Эта область представляет собой виртуальное адресное пространство, которое в 32-битном защищенном режиме всегда имеет размер, равный 4 гигабайтам.

Соответствие между виртуальным пространством и физической памятью

описывается с помощью таблицы страниц ОП.

 

Ядро ОС создает и заполняет таблицы, а процессор обращается к ним когда нужно выполнить "перевод" адреса. Каждый процесс работает со своим набором таблиц.

Концепция виртуальной адресации распространяется на все выполняемые программы, включая и само ядро. По этой причине для ядра резервируется часть виртуального адресного пространства (т.н. kernel space).

Динамическая память выделяется из сегмента кучи самой программы.

Каталоги страниц всех процессов организованы так, что преобразование линейного в физический адрес для системного диапазона любого адресного пространства совпадают – диапазон ядра «общий» для всех адресных пространств

В Linux kernel space всегда присутствует в памяти процесса, и разные процессы отображают kernel space в одну и ту же область физической памяти. Таким образом, код и данные ядра всегда доступны, если нужно обработать прерывание или системный

вызов.

2

 

 

Структура памяти (адресного пространства) запущенной программы (процесса)

 

 

 

Верхние адреса

 

Аргументы командной строки

Исполняемые файлы:

 

 

Переменные окружения

ELF-файлы (Executable and Linkable

 

 

 

 

 

 

Format ─ формат исполняемых и

 

 

Cтек (Stack)

связываемых файлов) в ОС Linux

 

 

 

 

 

 

PE-файлы (Portable Executable ─

 

 

 

«переносимый исполняемый») ─

 

 

 

формат исполняемых файлов,

 

Куча и стек "растут" по направлению друг к другу

объектного кода и динамических

 

библиотек (DLL), используемый в 32- и

 

 

 

 

 

 

64-разрядных ОС Windows

 

 

Куча (Heap) / Блок Динамической

БДП или

 

 

динамическая область распределения

 

 

Памяти (БДП)

Data Segment

 

памяти

 

 

RO-DATA

Неинициализированные данные (BSS)

 

 

 

 

RW-DATA

Инициализированные данные (Data)

Код (Text) / Code Segment

Нижние адреса

3

Стек и куча

 

Стек реализуется по правилу

 

 

 

LIFO (Last In First Out

 

 

 

«Последним пришёл —

 

 

 

первым ушёл»).

 

 

 

Последний

 

 

 

 

зарезервированный блок

 

 

 

всегда является следующим

 

 

 

освобождаемым блоком.

 

 

 

Это упрощает отслеживание

 

 

 

стека.

 

 

 

 

Освобождение блока из

Стек работает быстрее,

 

стека – это не что иное, как

 

потому что вся

 

настройка указателя.

 

свободная память всегда

 

 

 

 

смежна.

Куча – это память, выделенная для динамического

 

 

Не нужно вести список

 

 

 

всех сегментов

 

распределения.

 

 

 

 

 

свободной памяти,

В отличие от стека, вы можете выделить блок в любое время и

 

достаточно одного

 

 

указателя на текущую

 

освободить его в любое время.

 

 

 

вершину стека.

В куче нет определенного порядка размещения элементов.

 

 

 

 

Вы можете войти и удалить элементы в любом порядке, потому

 

 

 

Стек – это память, выделенная (в качестве свободного

 

 

что нет выделенного "верхнего" элемента как в стеке.

 

 

места) для выполняемого потока.

 

 

Это значительно усложняет отслеживание того, какие части кучи

 

 

 

 

 

 

выделяются или освобождаются в любой момент времени.

Стек тесно связан с архитектурой процессора.

Существует множество пользовательских распределителей

Увеличение кучи, когда не хватает места, не слишком сложно,

 

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

так как это может быть реализовано в вызове библиотеки,

 

различных моделей использования.

который обрабатывает кучу. Однако увеличение стека часто

Память для кучи (как правило) выделяется ОС, и для этого

невозможно, так как переполнение стека обнаруживается

 

приложение вызывает функции API.

только тогда, когда становится слишком поздно; и завершение

При управлении динамически выделяемой памятью (кучей)

потока выполнения является единственным жизнеспособным

 

вариантом.

 

 

 

требуются значительные накладные расходы.

 

 

 

Обычно максимальный размер уже определен при запуске

 

 

 

 

 

 

вашей программы (1МБайт).

4

Иерархия рабочих единиц ОС. Место Кучи

Куча / Heap

Куча (heap) в информатике и программировании

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

Куча

В

Ы

Стек

Д

Е

 

Л

 

Я

 

Ю

 

Т

Стек

С

Я

 

Каждый поток получает стек, в то время как (обычно) для приложения существует только одна куча

Стандартная библиотека Си

Диспетчер кучи

void *malloc(…) { … } void free(…) { … }

список занятых блоков

список свободных блоков

Куча имеет свои преимущества и недостатки:

Выделение памяти в куче сравнительно медленное.

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

Доступ к динамически выделенной памяти

осуществляется только через указатель:

разыменование указателя происходит медленнее, чем доступ к переменной напрямую.

Куча используется при динамическом выделении памяти для массивов, структур или классов.

Размер кучи — размер памяти, выделенный операционной системой для хранения кучи (под кучу,

как правило = 1МБайт).

5

Вспомним про указатели на указатели (можно пропустить)

Указатели, как и переменные любого другого типа, могут объединяться в массивы.

Объявление массива указателей р на целые числа: int *р[10], y;

Теперь каждому из элементов массива указателей р можно присвоить адрес переменной y, например: р[1] = &y;

Чтобы найти значение переменной y через данный элемент массива р, необходимо записать *р[1].

В языке Си можно описать переменную типа «указатель на указатель».

Это ячейка памяти (переменная), в которой будет храниться адрес указателя на некоторую переменную.

Признак такого типа данных – повторение символа «*» перед именем переменной.

Количество звездочек определяет уровень вложенности указателей друг в друга.

При объявлении указателей на указатели возможна их инициализация.

Например:

int a = 5; int *p = &a;

int **pp = &p; int ***ppp = &pp;

Если присвоить переменной а новое значение: a=10; то следующие величины будут иметь такие же значения 10:

*p

**pp

***ppp

Для доступа к памяти, отведенной под переменную а можно использовать и индексы, т.е. следующие выражения - эквивалентны :

*p

~

p[0] ;

**pp

~

pp[0][0] ;

***ppp

~

ppp[0][0][0] .

Фактически, используя указатели на указатели, мы имеем дело с многомерными массивами.

Для чего нужны указатели:

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

для адресации данных, размещенных в динамической памяти (куче) на этапе выполнения программы

для создания сложных структур данных, элементы которых содержат ссылки друг на друга (списки, деревья, графы, ...)

Правда об операции [ ]

Операция индексации [] – это сложение указателя со смещением (индексом) и последующее разыменование!

int arr[10] = {0}; arr[5] = 42;

*(arr + 5) = 42; // читаем справа-налево перед “=”

5[arr] = 42;

int *ptr = &5[arr];

ptr[1] = 2[ptr] = *(ptr + 3) = 42; ptr++;

ptr[-1] = -2[ptr] = *(ptr - 3) = 42;

6

Память для глобальных переменных распределяется на этапе компиляции и выделяется при загрузке программы в память.

Память для локальных переменных выделяется в области стека в момент начала выполнения функции.

Мы не можем в процессе выполнения программы объявить новые локальные или глобальные переменные, хотя необходимость этого может выясниться только в процессе выполнения.

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

Механизм динамического распределения памяти позволяет программе получать необходимую для хранения данных память в процессе своего выполнения.

Для получения доступа к динамически выделяемым областям памяти и их типизации служат указатели.

Динамическое распределение памяти

Данные (классификация по времени жизни)

Статические (глобальные и описанные как static):

описываются вне функций или с помощью static;

распределяются в памяти на этапе компиляции и существуют все время выполнения программы;

место в памяти – статический сегмент;

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

 

Автоматические:

 

Динамические:

описываются в

 

описываются не

 

функциях (без static);

 

данные, а их адреса

распределяются в

 

(указатели);

 

памяти на этапе

 

распределяются и

 

выполнения (при

 

уничтожаются в памяти

 

каждом вызове

 

на этапе выполнения

 

подпрограммы) и

 

программы по

 

освобождают память

 

специальным

 

при завершении

 

командам;

 

работы программы;

 

место в памяти –

место в памяти – стек

 

динамическая память

 

функций;

 

(куча – heap);

доступны в блоке

 

время жизни и область

 

функции.

 

действия указателей

 

 

 

определяется как для

 

 

 

 

 

обычных (статических

 

 

 

или автоматических)

 

 

 

данных.

 

 

 

 

7

 

 

1. Динамическая память

 

 

 

 

Перед тем, как начать разговор о динамической работе с

Структура памяти запущенной программы (процесса):

памятью, необходимо рассмотреть структуру приложения.

 

 

 

 

 

Верхние адреса

Память запущенной программы (процесса) разделена на ряд

 

Аргументы командной строки

 

 

непрерывных блоков (сегментов). Кроме Kernel Space существуют

 

 

 

Переменные окружения

 

следующие сегменты:

 

 

 

 

 

 

 

 

Сегмент данных (Data, bss-сегмент и куча/heap/БДП):

 

 

Cтек (Stack)

ELF-файлы в

Data состоит из статических и глобальных переменных,

 

 

ОС Linux

 

 

 

 

 

 

 

которые явно инициализируются значениями.

 

 

 

 

 

 

Этот сегмент может быть далее разбит на:

 

 

 

 

 

PE (Portable

 

RO-DATA (Read Only DATA) – сегмент данных только для

Куча и стек "растут" по направлению друг к другу

 

 

чтения, и

 

 

 

 

 

Executable)-

 

RW-DATA (Read Write DATA) – сегмент данных для чтения

 

 

 

 

 

файлы в

 

и записи.

 

Куча (Heap) / Блок Динамической

ОС Windows

 

Например, глобальные переменные

Segment

 

 

 

 

Памяти (БДП)

 

функция, вызвавшая выделение этой памяти, завершит работу (в

 

 

 

 

 

 

 

данные (BSS)

 

 

Куча (Heap): используется для выделения памяти во время

 

 

 

 

 

БДП или

 

выполнения программы (динамические переменные).

 

 

 

Неинициализированные

динамическая

 

Память, выделенная в куче, сохраняется и после того, как

 

RO-DATA

 

 

 

 

область

 

отличие от стека)

Data

 

 

 

 

распределения

 

 

 

Инициализированные

памяти

Cтек (Stack):

RW-DATA

 

Область памяти, используемая для хранения локальных

 

 

данные (Data)

 

 

 

 

 

 

 

переменных и аргументов, переданных в функцию.

 

 

 

 

 

 

Добавление данных в стек и их удаление – операция

 

Код (Text) / Code Segment

 

 

быстрая.

 

 

 

 

 

 

 

 

 

Кроме того, многократное использование одних и тех же

 

 

 

 

 

Нижние адреса

 

областей стека приводит к тому, что они помещаются в кеш

Утилита size показывает размер разделов и общий размер для

 

процессора, что еще более ускоряет доступ к ним.

Сегмент кода:

объектных файлов или архивов.

 

В нем находится исполняемый код программы.

Так, для memsegments.o получим:

 

Содержимое сегмента доступно для чтения/исполнения.

$ size memsegments.o

 

 

Благодаря тому, что этот сегмент защищен от записи,

text

data

bss

dec

hex filename

 

 

программа не может испортить свой собственный код во

745

12

8

765

2fd memsegments.o

 

 

время выполнения.

 

 

 

 

 

8

Динамическая память

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

При динамическом распределении памяти объекты размещаются в т. н. «куче» (heap): при «конструировании» объекта указывается размер запрашиваемой под объект памяти, и, в случае успеха, выделенная область памяти, условно говоря, «изымается» из «кучи», становясь недоступной при последующих операциях выделения памяти.

Противоположная по смыслу операция — освобождение занятой ранее под какой-либо объект памяти:

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

По мере создания в программе новых объектов количество доступной памяти уменьшается (размер кучи по умолчанию =

1МБ)

Отсюда вытекает необходимость постоянно освобождать ранее выделенную память.

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

По аналогии с этим, каждая подпрограмма (процедура или функция) должна обеспечить освобождение всей памяти, выделенной в ходе её выполнения.

Неправильное управление памятью приводит к т.н. «утечкам» памяти, когда выделенная память не освобождается.

Многократные утечки памяти могут привести к исчерпанию всей оперативной памяти и нарушить работу операционной системы.

Другая проблема — это проблема фрагментации памяти.

Выделение памяти происходит блоками — непрерывными фрагментами оперативной памяти (таким образом, каждый блок — это несколько идущих подряд байт).

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

В Java/C# и т.д. для управления динамическим распределением памяти используется «сборщик мусора» — программный объект, который следит за выделением памяти и обеспечивает её своевременное освобождение.

Сборщик мусора также следит за тем, чтобы свободные блоки имели максимальный размер, и, при необходимости,

осуществляет дефрагментацию памяти.

9

2. Функции для динамического распределения памяти

Отметим, что в самом языке, средств работы с динамической памятью нет: они вытеснены в библиотеку.

В языке программирования Си имеются следующие функции для динамического распределения памяти, входящие в стандартную библиотеку (#include <stdlib.h>):

malloc (memory allocation, выделение памяти),

calloc (clear allocation, чистое выделение памяти)

realloc (reallocation, перераспределение памяти).

free (free, освободить).

При запуске процесса ОС выделяет память для размещения кучи (как правило 1Мбайт).

В дальнейшем память для кучи (под кучу) может выделяться динамически.

Программа пользователя, используя функции, подобные malloc(), может получать указатели на области памяти, принадлежащие куче.

Программы используют кучу для размещения динамически создаваемых структур данных. Программа может освободить память с помощью функций, подобных free().

Память кучи можно разделить на занятую (выделенную программе с помощью функций, подобных malloc()) и свободную (ещё не занятую или уже освобождённую с помощью функций, подобных free()).

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

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

Функция malloc() выполняет (примерно) следующие действия:

просматривает список занятых/свободных областей памяти, размещённых в куче, в поисках свободной области подходящего размера;

в случае нехватки свободной памяти может запросить дополнительную память у ОС;

добавляет найденную область в список занятых областей (или помечает область как занятую);

возвращает указатель на начало области памяти;

если по тем или иным причинам выделить память не удалось, сообщает об ошибке (например, malloc() возвращает NULL).

Функция free() выполняет (примерно) следующие действия:

просматривает список занятых/свободных областей памяти, размещённых в куче, в поисках указанной области;

удаляет из списка занятых областей памяти найденную область;

добавляет найденную область в список свободных областей (или помечает область как свободную).

Программа (процесс) пользователя может быть «уверена» в том, что между вызовами функций, подобных malloc() и free(), область памяти, помеченная как занятая, не будет выделена повторно.

После вызова функции, подобной free(), область памяти будет освобождена и в дальнейшем может использоваться повторно или может быть отдана ОС.

Использование указателя на освобождённую область памяти будет приводить к сбоям или непредсказуемой работе

программы.

10

Соседние файлы в папке Лекции