Software

Программирование на языках, которые позволяют взаимодействовать с памятью на более низком уровне, как например в C и C++, иногда доставляет немало проблем, с которыми вы раньше не сталкивались, например: segfaults(ошибки сегментации). Такие ошибки очень раздражают, и могут стать причиной множества проблем; часто они свидетельствуют о том, что вы используете память, которую не следует использовать.

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

Поняв некоторые аспекты, связанные с памятью, вы станете действительно лучше и умнее в программировании!

Как разделена память

 

Память разделена на несколько сегментов, два из наиболее важных (для этой статьи) — это стек и heap. Стек — это упорядоченная область внедрения, а heap полностью произвольная ― вы резервируете память там, где это возможно.

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

Сегмент heap часто используется для резервирования большого объёма памяти. Этот резерв существует столько, сколько потребуется разработчику. Другими словами, контроль памяти в сегменте heap предоставлен разработчику. Разрабатывая сложные программы, часто приходится резервировать большие части памяти. Именно для этого и предназначен сегмент heap. Мы называем это ― Динамическая память.

Каждый раз, когда вы используете malloc, чтобы разместить что-либо в памяти – вы обращаетесь к сегменту heap. Любой другой вызов, например int i;, относится к стек памяти. Очень важно это понимать, чтобы с лёгкостью находить ошибки, в том числе ошибки сегментации.

Понимание стека

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

Как это работает изнутри?

Стек имеет структуру данных LIFO (Last-In-First-Out) ― «последним пришёл –первым ушёл». Представьте аккуратную стопку книг. В этой стопке вы можете взять первой, ту книгу, которую положили последней. Такая структура позволяет программе управлять своими операциями и пространствами, двумя простыми командами: push и pop. Команда push добавляет значение сверху стека, а pop наоборот ― изымает значение.

 

Для отслеживания текущего места в памяти существует специальный регистр процессора ― Stack Pointer (указатель стека). Например, переменные или обратный адрес из функции при сохранении попадают в стек и stack pointer перемещается вверх. Завершение функции означает, что всё изымается из стека, начиная с текущего положения stack pointer, до сохранённого обратного адреса из функции. Всё просто!

Чтобы проверить, всё ли вы поняли, давайте используем следующий пример (попробуйте найти ошибку самостоятельно ☺️):

 

Если запустить программу, она выдаст ошибку сегментации. Почему так происходит? Кажется, что всё на своих местах! За исключением… стека.

Когда мы вызываем функцию createArray, стек сохраняет обратный адрес, создаёт arr в памяти стека и возвращает arr (массив — это просто указатель на область памяти с этой информацией). Но, так как мы не использовали mallocarr остаётся в стек памяти. После того как мы возвращаем указатель, так как мы не контролируем операции стека, программа изымает информацию из стека и использует её для своих нужд. Когда мы пытаемся заполнить массив, после того как вернули его из функции, мы повреждаем память и получаем ошибку сегментации.

Понимание кучи (heap)

В отличии от стека, в куче сохраняется что-либо, независимо от функций и пространств, в течение необходимого времени. Для использования этой памяти, в языке C есть библиотека stdlib с функциями malloc и free.

Malloc (memory allocation) запрашивает у системы необходимый объем памяти и возвращает указатель на начальный адрес. Free сообщает системе, что запрошенная память больше не нужна и может быть использована для других задач. Выглядит действительно просто ― до тех пор, пока вы избегаете ошибок.

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

Утечка произойдёт, если память не освободить вовремя, до завершения выполнения программы или если указатели на расположение этой памяти были потеряны. Программа будет использовать больше памяти чем должна. Чтобы этого не случалось, мы должны освобождать выделенную в куче память, когда она нам не нужна.

 

На изображении видно, как в «bad» версии кода, мы не освобождаем память. Это приводит к растрачиванию 20 * 4 байт (размер int 64-бит) = 80 байт. Может показаться, что это незначительно, но, если это большая программа, речь может идти о гигабайтах!

Чтобы программа эффективно использовала память — важно контролировать память кучи. В тоже время, делать это нужно с осторожностью. Если память была освобождена, то попытка получения доступа к ней или её использование может привести к ошибке сегментации.

Бонус: Struct и куча

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

 

Как я устраняю утечки памяти

Программируя на C, я часто использую struct, поэтому у меня всегда под рукой две необходимые функции: конструктор и деструктор. Это единственные функции, для которых я использую mallocs и frees в struct. Это упрощает решение проблем с утечкой памяти.

 

Перевод статьи Tiago AntunesUnderstand your program’s memory