Перейти к основному содержанию

2. Управление памятью

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

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

Стек и куча

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

Стек – это организация хранения данных по принципу LIFO, последний пришёл – первый ушёл. Аналогия физического мира:  стакан, в который вы складываете круглые печеньки, чтобы достичь первой печеньки, вам нужно вытащить все предыдущие.

Стек быстрый, но предназначен для хранения одинаковых по размеру данных. Причем размер этих данных (размер элемента стека) должен быть известен уже во время компиляции. Есть 2 операции для работы со стеком: push (толкнуть) – поместить в него данные и pop (popped - выскочить) – забрать данные.

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

Аналогия физического мира: хранение вещей на автоматизированном складе, в котором у нас есть записи о том где хранятся те или иные товары. И мы можем забрать товар в определенном ряду с определенной полки, и не важно это маленькая коробочка или целая палета. Другой аналог – библиотека. В ней книги хранятся на полках в хранилище, и есть шкафчики с карточками на которых написано где стоит нужная вам книга.

Пример данных хранимых в куче:

fn main() {
	let s: String = "Некий текст".to_string();
	println!("{}", s);
}
Результат: Некий текст

Переменная s это динамически изменяемая структура, и до начала выполнения программы мы не знаем сколько места в памяти будет занимать переменная типа String (Конкретно в этом примере можно высчитать количество символов, но строка, может быть введена из консоли или прочитана из файла). Поэтому в переменную записывается:

  • ptr (pointer)   – указатель, это адрес начальной ячейки памяти в памяти, например, 0x2061A686B40.

  • len – длина значения, в данном случае 21 байт(кириллические буквы по 2 байта и пробел 1 байт).

  • capacity – емкость, т.е. сколько выделено памяти для записи, например, 24 байт.

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

Проблемы при работе с памятью

Память (ОЗУ) компьютера ограничена по размеру, поэтому следует рационально относиться к её использованию. Программисты могут допускать ошибки, которые приводят к проблемам при работе программ:

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

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

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

Как решаются проблемы в других языках

В некоторых языках программист должен явно выделять и освобождать конкретные участки памяти. Это сложно и предоставляет возможность допустить ошибку. В других языках программист вообще не задумывается о выделении и освобождении памяти, это делает за него программа “сборщик мусора”. Но такая программа регулярно прерывает выполнение основного потока и замедляет работу программы. Нам требуется иной подход.

Безопасная работа с памятью в Rust

В Rust нет сборщика мусора и программисту необходимо управлять памятью. Но управление памятью проще, чем в C-подобных языках, благодаря, концепции владения и строгости компилятора, который предупредит об ошибке на этапе компиляции. Об этом в следующих лекциях.

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

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

© Клют И. А., 2022. Копирование контента возможно только с письменного разрешения автора.