Память
Предпосылки: Функции (вызов функций, локальные переменные, область видимости), Коллекции (массивы, хеши).
← Коллекции | Объектно-ориентированное программирование →
В прошлой заметке про коллекции массивы и хеши выглядели как обычные значения. Но с ними быстро появляется странность:
orders = [1200, 900, 1400]
copy = orders
copy[3] = 1800
puts orders # [1200, 900, 1400, 1800]Почему изменился orders, если меняли copy? Чтобы это понять, нужна модель памяти. Без неё человеку трудно отличить две независимые структуры данных от двух имён для одного и того же значения в памяти.
В этой заметке Ruby нужен как компактная запись примеров. Та же модель проявляется и в других языках: C показывает адреса явно, Ruby прячет их за обычным присваиванием, Rust добавляет поверх этого правила владения.
Имена, ссылки и данные
Для массивов, хешей и других составных данных в этой заметке удобно держать одну модель: переменная хранит не весь набор данных внутри себя, а способ добраться до него.
orders ----\
> [1200, 900, 1400]
copy ----/После copy = orders обе переменные указывают на один и тот же массив. Поэтому copy[3] = 1800 меняет те же данные, которые видны и через orders.
Проблема здесь не в процессоре. Проблема в том, что без такой модели памяти легко ошибиться при чтении кода: кажется, что переменных две, а данных на самом деле один набор.
Стек: где живут локальные имена вызова
Когда вызывается функция, программе нужно где-то держать параметры и локальные имена именно этого вызова. Для этого используют стек.
def add(a, b)
result = a + b
result
end
def main
x = 10
y = 20
sum = add(x, y)
puts sum
endmain():
Стек: [ main: x=10, y=20 ]
main() вызывает add(10, 20):
Стек: [ main: x=10, y=20 | add: a=10, b=20, result=30 ]
add() возвращает 30:
Стек: [ main: x=10, y=20, sum=30 ]Пока работает add, у программы есть локальные имена a, b и result. После возврата из add они исчезают. Стек хорош именно для таких краткоживущих вещей: параметров, временных значений и порядка возврата из вызовов.
Куча: где живут данные дольше вызова
Одних только локальных имён недостаточно. Массивы, хеши и другие составные данные часто должны переживать отдельный вызов функции, поэтому сами данные обычно хранятся отдельно от стека, в куче (heap — область без строгого порядка, в отличие от стопки стека).
имя в стеке ----\
> ссылка -> данные в куче
другое имя -----/Поэтому массив, на который указывают orders и copy, не исчезает вместе с каким-то одним именем. Исчезнуть должны все ссылки на него, только тогда память под эти данные можно считать больше не нужной.
Две разные операции: переназначить имя и изменить общие данные
Самая частая путаница появляется при вызове функций. Важно различать две операции:
- переназначить локальное имя на другие данные;
- изменить уже существующие общие данные.
def replace_list(list)
list = [0]
end
def append_to_list(list)
list << 4 # << добавляет элемент в конец массива
end
numbers = [1, 2, 3]
replace_list(numbers)
puts numbers # [1, 2, 3]
append_to_list(numbers)
puts numbers # [1, 2, 3, 4]В replace_list имя list просто начинает указывать на новый массив [0]. У вызывающего кода имя numbers всё ещё указывает на старые данные.
В append_to_list меняются сами данные, на которые указывают и list, и numbers. Поэтому изменение видно снаружи.
То же различие видно и на маленьких неизменяемых значениях:
def double(n)
n = n * 2
end
x = 5
double(x)
puts x # 5Здесь тоже происходит переназначение локального имени n, а не изменение значения у вызывающего кода.
Одна модель, разные языковые формы
Ruby. Ссылки обычно скрыты. Программист пишет copy = orders, а модель совместного доступа остаётся за обычным присваиванием.
C. Адреса видны прямо в коде. Указатель — это значение, которое хранит адрес участка памяти:
int salary = 1500;
int *p = &salary;
printf("%d\n", *p);
*p = 2000;
printf("%d\n", salary);&salary означает «взять адрес salary». *p означает «прочитать или изменить значение по этому адресу».
Rust. Сам адрес обычно не показывают напрямую, но язык жёстко отвечает на другой вопрос: кто сейчас отвечает за это значение.
fn main() {
let s = String::from("hello");
let t = s;
println!("{}", t);
}После let s = String::from("hello") строкой владеет s. После let t = s владение переходит к t, поэтому s больше нельзя использовать. Когда t выйдет из области видимости, строка будет освобождена.
Во всех трёх случаях вопрос один и тот же: сколько имён ведёт к одним данным, кто имеет право их менять и когда эти данные должны исчезнуть.
Кто освобождает память
После того как данные перестали быть нужны, остаётся ещё один вопрос: кто обязан их освободить.
C: ручное управление. Программист сам выделяет память и сам же обязан её освободить.
int *arr = malloc(5 * sizeof(int));
arr[0] = 10;
free(arr);Если забыть free, память остаётся занятой. Если вызвать free, а потом продолжить пользоваться старым адресом, программа начинает читать или писать уже не свои данные.
Ruby: сборщик мусора. Ruby освобождает данные автоматически: когда на них больше никто не ссылается, их позже найдёт и удалит сборщик мусора (garbage collector). Программисту не нужно помнить про free, но рантайм периодически тратит время на поиск неиспользуемых данных.
Rust: освобождение через владение. Когда владелец значения выходит из области видимости, память освобождается автоматически. Программист не пишет free, но и не может произвольно дублировать доступ к изменяемым данным: язык следит за этим заранее.
Общая модель одна и та же: у программы есть имена, данные и время жизни данных. Языки отличаются тем, насколько явно показывают адреса и кто несёт ответственность за освобождение памяти и изменение общих структур.
Ссылки и мутация объясняют, почему copy[3] = 1800 из начала заметки затронул orders. Но они же создают следующий вопрос: если одни и те же данные разделяют несколько частей программы, кто следит за тем, чтобы их не сломали? Это вопрос организации кода — ответ даёт объектно-ориентированное программирование.
Sources
- Kernighan, B. & Ritchie, D., 1988, The C Programming Language. Prentice Hall.
- Klabnik, S. & Nichols, C., 2023, The Rust Programming Language. No Starch Press.
- Thomas, D. et al., 2023, Programming Ruby 3.3. Pragmatic Bookshelf.