$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогMemory management в Rust: стек, куча и когда данные действительно удаляются

Memory management в Rust: стек, куча и когда данные действительно удаляются

$ sudo teach IT
·18 июля 2026 г.·9 мин·66
Memory management в Rust: стек, куча и когда данные действительно удаляются

Объясняем, как Rust управляет памятью без сборщика мусора — через владение, время жизни и Drop. Сравниваем с поведением C++ и показываем, почему подход Rust исключает целый класс ошибок.

Содержание
Стек и куча: не просто теорияСтекКучаВладение: правила, которые меняют всёMove-семантикаЗаимствование (borrowing)Времена жизни: явная семантика временностиЯвная аннотация времён жизниDrop: детерминированное уничтожениеПорядок уничтоженияСравнение с C++: где именно возникают ошибкиUse-after-freeDouble-freeDangling pointersIterator invalidationУмные указатели: когда одного владельца недостаточноUnsafe: честный выход из системыПрактические следствия для разработчикаВывод

Управление памятью — одна из тех тем, где разные языки программирования расходятся принципиально, а не в деталях. Python и Java прячут проблему за сборщиком мусора. C и C++ перекладывают её целиком на программиста. Rust выбрал третий путь: строгая система типов и правила компилятора, которые делают целый класс ошибок физически невозможным — без рантаймовых издержек GC и без ручного free.

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


Стек и куча: не просто теория

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

Стек

Стек работает по принципу LIFO: последний вошёл — первый вышел. Каждый раз, когда вы вызываете функцию, рантайм выделяет на стеке фрейм — блок памяти под локальные переменные, аргументы и адрес возврата. Когда функция завершается, фрейм просто «снимается» со стека. Никакого поиска свободного места, никакой фрагментации.

Это работает быстро по одной причине: размер всех данных на стеке известен на этапе компиляции. i32 занимает 4 байта — всегда. [u8; 1024] занимает 1024 байта — всегда. Компилятор знает, сколько места нужно выделить ещё до запуска программы.

code
fn calculate(x: i32, y: i32) -> i32 {
    let result = x + y; // result живёт на стеке
    result              // возвращаем значение — копируем его
}                       // фрейм уничтожается, result исчезает

Куча

Куча — это большой пул памяти, из которого программа запрашивает блоки произвольного размера в рантайме. Именно здесь живут данные, чей размер неизвестен заранее: строки переменной длины, векторы, объекты, которые должны пережить вызов функции.

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

code
fn make_greeting(name: &str) -> String {
    let greeting = format!("Hello, {}!", name); // данные в куче
    greeting // возвращаем — передаём владение
}

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


Владение: правила, которые меняют всё

Система владения (ownership) — сердце управления памятью в Rust. Три правила, которые компилятор проверяет статически:

  1. У каждого значения есть ровно один владелец.
  2. Когда владелец выходит из области видимости, значение уничтожается.
  3. Владение можно передать (move), но не скопировать неявно — если тип не реализует Copy.

Звучит просто. На практике это означает, что компилятор точно знает, в какой момент нужно освободить память — и делает это детерминированно, без GC.

Move-семантика

code
fn main() {
    let s1 = String::from("hello"); // s1 владеет строкой
    let s2 = s1;                    // владение передаётся s2
    
    // println!("{}", s1); // ошибка компиляции: s1 больше не валиден
    println!("{}", s2);   // OK
}

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

Заимствование (borrowing)

Передавать владение при каждом вызове функции неудобно. Для этого есть заимствование — временный доступ к данным без передачи владения:

code
fn print_length(s: &String) {
    println!("Length: {}", s.len()); // читаем, но не владеем
}

fn main() {
    let s = String::from("hello");
    print_length(&s); // передаём ссылку
    println!("{}", s); // s всё ещё валиден
}

Ключевое правило заимствования: в любой момент времени может существовать либо любое количество неизменяемых ссылок (&T), либо ровно одна изменяемая ссылка (&mut T). Никогда — и то, и другое одновременно.

Это правило на уровне типов исключает data races в многопоточном коде и iterator invalidation в однопоточном.


Времена жизни: явная семантика временности

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

Классическая ошибка в C++:

code
// C++ — undefined behavior
const char* get_name() {
    std::string name = "Alice";
    return name.c_str(); // возвращаем указатель на уничтоженные данные
}

Компилятор C++ это пропустит. Программа упадёт или выдаст мусор — в рантайме, возможно на продакшне.

В Rust эквивалентный код не скомпилируется:

code
// Rust — ошибка компиляции
fn get_name() -> &str {
    let name = String::from("Alice");
    &name // ошибка: name уничтожается, ссылка становится dangling
}

Компилятор скажет: name does not live long enough. Это не предупреждение — это ошибка, которая блокирует сборку.

Явная аннотация времён жизни

В большинстве случаев компилятор выводит времена жизни автоматически (lifetime elision). Но иногда нужна явная аннотация:

code
// Возвращаем ссылку на тот из двух аргументов, который длиннее.
// Компилятор должен знать: результат живёт не дольше, чем оба аргумента.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Аннотация 'a — это не указание продолжительности жизни, а ограничение: «результат живёт не дольше, чем пересечение времён жизни x и y». Компилятор использует эту информацию, чтобы проверить корректность использования результата.


Drop: детерминированное уничтожение

В C++ деструктор вызывается, когда объект выходит из области видимости. В Rust аналог — трейт Drop:

code
struct DatabaseConnection {
    id: u32,
}

impl Drop for DatabaseConnection {
    fn drop(&mut self) {
        println!("Closing connection {}", self.id);
        // здесь — реальное закрытие соединения
    }
}

fn main() {
    let conn = DatabaseConnection { id: 42 };
    println!("Using connection");
} // drop вызывается здесь автоматически

Вывод:

code
Using connection
Closing connection 42

Это детерминированное уничтожение: вы точно знаете, когда ресурс будет освобождён. В отличие от GC-языков, где финализатор может вызваться когда угодно — или не вызваться вовсе до завершения программы.

Порядок уничтожения

Rust гарантирует порядок: переменные уничтожаются в обратном порядке объявления. Поля структуры — в порядке объявления. Это предсказуемо и задокументировано.

code
fn main() {
    let first = DatabaseConnection { id: 1 };
    let second = DatabaseConnection { id: 2 };
    let third = DatabaseConnection { id: 3 };
} 
// Порядок drop: third (3), second (2), first (1)

Сравнение с C++: где именно возникают ошибки

Чтобы оценить подход Rust, полезно посмотреть на конкретные классы ошибок, которые он исключает.

Use-after-free

code
// C++
int* ptr = new int(42);
delete ptr;
std::cout << *ptr; // undefined behavior — читаем освобождённую память

В Rust Box<T> — умный указатель, который владеет данными в куче. После его уничтожения использовать содержимое невозможно — компилятор не даст.

code
// Rust
let b = Box::new(42);
drop(b);
// println!("{}", b); // ошибка компиляции: b moved/dropped

Double-free

code
// C++
int* ptr = new int(42);
delete ptr;
delete ptr; // undefined behavior

В Rust невозможно вызвать drop дважды на одно значение — система владения гарантирует единственного владельца.

Dangling pointers

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

Iterator invalidation

code
// C++
std::vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // может реаллоцировать вектор — it становится невалидным
std::cout << *it; // undefined behavior

В Rust правило заимствования делает это невозможным: нельзя иметь изменяемую ссылку на вектор (push_back требует &mut) одновременно с неизменяемой ссылкой на его элемент.

code
let mut v = vec![1, 2, 3];
let first = &v[0];   // неизменяемое заимствование
v.push(4);           // ошибка: нельзя изменять v, пока есть &v[0]
println!("{}", first);

Умные указатели: когда одного владельца недостаточно

Иногда данные действительно нужно разделить между несколькими владельцами. Для этого в стандартной библиотеке есть Rc<T> и Arc<T>.

code
use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    let clone1 = Rc::clone(&data);
    let clone2 = Rc::clone(&data);
    
    println!("Reference count: {}", Rc::strong_count(&data)); // 3
} 
// Данные уничтожаются, когда счётчик достигает 0

Rc<T> — reference counting для однопоточного кода. Arc<T> — атомарный вариант для многопоточного. Это не GC: счётчик обновляется детерминированно, уничтожение происходит в момент, когда последний владелец выходит из области видимости.

Важный нюанс: Rc<T> не решает проблему циклических ссылок — для этого есть Weak<T>. Это один из редких случаев, где Rust не даёт автоматической защиты, и программист должен думать об архитектуре.


Unsafe: честный выход из системы

Rust не притворяется, что его система типов покрывает все случаи. Для низкоуровневого кода — работы с сырыми указателями, FFI, специфических оптимизаций — есть блок unsafe:

code
fn main() {
    let x = 42i32;
    let ptr = &x as *const i32; // сырой указатель
    
    unsafe {
        println!("{}", *ptr); // разыменование — только в unsafe
    }
}

unsafe не отключает borrow checker — он разрешает конкретные операции, которые компилятор не может проверить статически. Это честный контракт: вы берёте ответственность за этот конкретный участок кода, а не за всю программу.

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


Практические следствия для разработчика

Система владения Rust меняет не только безопасность, но и стиль мышления. Несколько практических наблюдений:

Ошибки компиляции — это документация. Когда borrow checker отклоняет код, он обычно указывает на реальную проблему с логикой владения данными. Это не формальность — это компилятор, который говорит: «ты не продумал, кто отвечает за этот ресурс».

Архитектура становится явной. В C++ можно написать код, где непонятно, кто владеет объектом. В Rust это невозможно — структура владения всегда выражена в типах.

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

Тем, кто хочет разобраться в этих концепциях системно — от базовых типов до многопоточности и async/await — стоит посмотреть на курс по Rust от «sudo teach IT»: там эти темы разбираются последовательно, с практическими задачами, а не только в теории.


Вывод

Rust не изобрёл новую математику. Move-семантика есть в C++11, RAII — ещё старше, reference counting — стандартный приём. Rust взял эти идеи и сделал их обязательными на уровне системы типов, убрав возможность случайно их нарушить.

Результат — язык, где целый класс ошибок памяти (use-after-free, double-free, dangling pointers, data races) исключён статически, а не обнаруживается в рантайме инструментами вроде Valgrind или AddressSanitizer. Это не серебряная пуля: кривая обучения крутая, borrow checker поначалу кажется враждебным, а некоторые паттерны требуют нетривиальных решений. Но когда код компилируется — он, как правило, работает корректно в части управления памятью.

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

Понимание того, как Rust управляет памятью, меняет то, как вы думаете о ресурсах в любом языке. Даже если вы в итоге пишете на Go или C++ — концепции владения и времён жизни становятся частью вашего инструментария мышления.

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Rust

Курс по теме

Rust

Rust — с нуля до уверенной работы с самым безопасным системным языком. Для начинающих и опытных. Освойте владение, трейты, многопоточность и async — и пишите надёжный код без страха утечек памяти.

Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

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

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

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

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Ownership и Borrowing в Rust: почему компилятор умнее вас (и это хорошо)
ownership

Ownership и Borrowing в Rust: почему компилятор умнее вас (и это хорошо)

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

16 июля 2026 г.
690
Rust Cargo для команды: workspace, общие зависимости и единые версии библиотек
rust

Rust Cargo для команды: workspace, общие зависимости и единые версии библиотек

Покажем, как организовать монорепо без хаоса: workspace, shared crates, настройка версий и feature flags. Статья поможет уменьшить “сюрпризы” при сборке на разных машинах.

26 июля 2026 г.
590
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

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

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

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

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Стек и куча: не просто теорияСтекКучаВладение: правила, которые меняют всёMove-семантикаЗаимствование (borrowing)Времена жизни: явная семантика временностиЯвная аннотация времён жизниDrop: детерминированное уничтожениеПорядок уничтоженияСравнение с C++: где именно возникают ошибкиUse-after-freeDouble-freeDangling pointersIterator invalidationУмные указатели: когда одного владельца недостаточноUnsafe: честный выход из системыПрактические следствия для разработчикаВывод