Rust Admin

Guia Completo de Stack vs Heap em Rust: Como a Memória é Gerenciada Já leu

Stack vs Heap em Rust: Como a Memória é Gerenciada O que são Stack e Heap? Stack e Heap são duas regiões de memória fundamentalmente diferentes. O Stack é uma estrutura de dados Last In, First Out (LIFO) que armazena dados de tamanho conhecido em tempo de compilação. Quando uma função é chamada, seus dados são empilhados; ao retornar, são removidos automaticamente. O Heap, por outro lado, é uma região de memória mais flexível para dados cujo tamanho pode variar em tempo de execução. Acessar dados no Heap é mais lento porque requer um ponteiro (endereço de memória), enquanto o Stack oferece acesso direto e muito mais rápido. Em Rust, essa distinção é crucial porque o sistema de ownership trabalha sobre essas duas regiões de formas distintas. Variáveis simples como inteiros, booleanos e referências vivem no Stack. Estruturas de dados que crescem dinamicamente, como , , , vivem no Heap com um ponteiro no Stack. Compreender essa divisão é essencial

Stack vs Heap em Rust: Como a Memória é Gerenciada

O que são Stack e Heap?

Stack e Heap são duas regiões de memória fundamentalmente diferentes. O Stack é uma estrutura de dados Last In, First Out (LIFO) que armazena dados de tamanho conhecido em tempo de compilação. Quando uma função é chamada, seus dados são empilhados; ao retornar, são removidos automaticamente. O Heap, por outro lado, é uma região de memória mais flexível para dados cujo tamanho pode variar em tempo de execução. Acessar dados no Heap é mais lento porque requer um ponteiro (endereço de memória), enquanto o Stack oferece acesso direto e muito mais rápido.

Em Rust, essa distinção é crucial porque o sistema de ownership trabalha sobre essas duas regiões de formas distintas. Variáveis simples como inteiros, booleanos e referências vivem no Stack. Estruturas de dados que crescem dinamicamente, como String, Vec, HashMap, vivem no Heap com um ponteiro no Stack. Compreender essa divisão é essencial para dominar Rust e evitar erros de borrow checker.

fn exemplo_stack_heap() {
    // Stack: tamanho fixo, conhecido em tempo de compilação
    let x: i32 = 5;  // 4 bytes no Stack
    let y: i32 = x;  // Cópia eficiente, outro valor no Stack

    // Heap: tamanho dinâmico, ponteiro armazenado no Stack
    let s1 = String::from("Rust");  // Dados "Rust" no Heap, ponteiro em s1 no Stack
    let s2 = s1;  // Ownership transferido, s1 não é mais válido

    println!("x = {}, y = {}, s2 = {}", x, y, s2);
    // println!("{}", s1); // ERRO: s1 não possui ownership mais
}

Ownership e Gerenciamento Automático de Memória

Rust não usa garbage collection como Python ou Java. Em vez disso, implementa o sistema de ownership que garante segurança de memória em tempo de compilação. A regra fundamental é: cada valor tem um único proprietário, e quando o proprietário sai de escopo, a memória é liberada automaticamente. Para dados no Stack, isso é trivial. Para dados no Heap, Rust chama automaticamente o método drop() quando o valor sai de escopo.

Quando você move um valor (transfere ownership), o anterior se torna inválido. Isso evita a libertação dupla de memória (double free), um bug clássico em C/C++. Para compartilhar acesso sem transferir ownership, Rust fornece referências imutáveis (&T) e mutáveis (&mut T). As referências vivem no Stack e apontam para dados, mas o borrow checker garante que nunca haja data races ou acesso inválido.

fn propriedade_em_acao() {
    let s1 = String::from("Hello");
    let s2 = s1;  // Ownership movido para s2
    // println!("{}", s1);  // ERRO: s1 já não é proprietário

    println!("{}", s2);  // OK: s2 é o proprietário agora
}

fn usando_referencias() {
    let s1 = String::from("Rust");
    let len = calcular_tamanho(&s1);  // Referência imutável
    println!("Tamanho de '{}': {}", s1, len);
}

fn calcular_tamanho(s: &String) -> usize {
    s.len()
    // 's' sai de escopo aqui, mas não libera memória
    // porque não é proprietário, apenas referencia
}

fn modificar_string(s: &mut String) {
    s.push_str(" é incrível");
}

fn usando_mutabilidade() {
    let mut s = String::from("Rust");
    modificar_string(&mut s);  // Referência mutável
    println!("{}", s);  // "Rust é incrível"
}

Stack vs Heap: Implicações de Performance

Dados no Stack são extremamente rápidos de alocar e desalocar porque é apenas mover um ponteiro (operação O(1)). O compilador conhece o tamanho exato em tempo de compilação, então a alocação é determinística. Heap, por sua vez, requer buscar bloco de memória disponível (mais lento), especialmente em programas com muita alocação dinâmica. Por isso, Rust incentiva colocar dados no Stack sempre que possível.

Tipos que implementam Copy (como inteiros, floats, booleanos) residem integralmente no Stack e são duplicados ao serem atribuídos, não movidos. Tipos mais complexos como String e Vec residem no Heap e usam move semantics por padrão, evitando cópias custosas. Você pode implementar Clone para duplicar explicitamente valores no Heap, mas isso tem custo de performance.

fn stack_vs_heap_performance() {
    // STACK: cópia rápida (implementa Copy)
    let a = 42i32;
    let b = a;  // Cópia eficiente: apenas 4 bytes copiados
    println!("a: {}, b: {}", a, b);  // Ambos válidos

    // HEAP: move (String não implementa Copy)
    let s1 = String::from("Stack?");
    let s2 = s1;  // Move eficiente: só o ponteiro foi transferido
    // println!("{}", s1);  // ERRO

    // Clone explícito (caro)
    let s3 = String::from("Clone");
    let s4 = s3.clone();  // Cópia custosa: String inteira duplicada no Heap
    println!("s3: {}, s4: {}", s3, s4);  // Ambos válidos agora
}

fn exemplo_vetores() {
    let mut v = Vec::new();  // Alocação no Heap
    v.push(1);
    v.push(2);
    v.push(3);
    // Internamente: [1, 2, 3] no Heap, com ponteiro + capacity + len no Stack

    let v2 = v;  // Move eficiente
    println!("{:?}", v2);
    // println!("{:?}", v);  // ERRO: v já não possui os dados
}

Ciclo de Vida (Lifetimes) e Referências

Rust formaliza o conceito de "tempo de vida" de uma referência através de lifetimes. Toda referência tem um lifetime (anotado com 'a, 'b, etc.) que indica por quanto tempo ela é válida. O borrow checker garante que referências nunca apontam para dados já liberados (dangling pointers). Embora lifetimes pareçam complexos no início, eles são determinados automaticamente pelo compilador em 99% dos casos graças às "lifetime elision rules".

A regra essencial é: se você retorna uma referência de uma função, ela deve referenciar dados que vivem pelo menos tão longo quanto a referência. Isso previne bugs de use-after-free que são comuns em C. Entender lifetimes é fundamental para trabalhar com referências seguramente e aproveitar o poder total de Rust.

fn exemplo_lifetime_simples(s: &String) -> usize {
    // Lifetime implícito: a referência é válida enquanto 's' é válido
    s.len()
}

fn exemplo_lifetime_explicito<'a>(s: &'a String) -> &'a str {
    // Retorna slice com mesmo lifetime que 's'
    &s[0..5]
}

fn demonstracao_correta() {
    let texto = String::from("Lifetime em Rust");
    let slice = exemplo_lifetime_explicito(&texto);
    println!("{}", slice);
    // 'texto' está vivo, então 'slice' é válido
}

// Isso causaria ERRO em tempo de compilação:
// fn dangling_reference() -> &'static String {
//     let s = String::from("Oops");
//     &s  // ERRO: 's' sai de escopo, referência fica inválida
// }

Conclusão

Stack e Heap são fundamentais para dominar Rust. O Stack oferece performance superior para dados de tamanho fixo, enquanto o Heap fornece flexibilidade para estruturas dinâmicas. O sistema de ownership elimina classes inteiras de bugs de memória ao transferir a responsabilidade para o compilador, não para o programador. Finalmente, lifetimes garantem que referências sejam sempre válidas, criando código seguro sem garbage collection.

Referências


Artigos relacionados