DEV Community

Manav Verma
Manav Verma

Posted on

Demystifying Memory in Rust: Ownership, Borrowing, and the Memory Landscape:

Memory management is very important for any program, but in Rust, it takes center stage. Unlike languages with automatic garbage collection, Rust grants you deep control over where and how data resides - a superpower fueled by the innovative concepts of ownership and borrowing.

Have a look at some of the memory management techniques used by other programming languages.

Image description

Before understanding the concept of ownership and borrowing, we need to know about stack and heap:

Stack:

Image description

The stack is a region of memory that grows and shrinks automatically as functions push and pop frames on it. It's used for static memory allocation, which includes local primitive variables and function pointers. The stack is managed by the CPU, which makes it faster to allocate and deallocate memory compared to the heap. However, the size of the stack is limited and determined at the start of each thread. It's important to note that you can't control the lifespan of memory on the stack; when a function exits, all of its stack memory is reclaimed.

Heap:

Image description

The heap is a memory region for dynamic allocation, managed manually. It allows variables to have a global, flexible lifespan. Memory can be allocated at runtime, used until freed by the programmer or program's end. Manual management can lead to errors like memory leaks or dangling pointers. When data is added to the heap, the allocator finds a sufficient empty area, marks it as used, and returns a pointer to the location. This pointer can be stored in the stack for future reference. To retrieve data, follow the pointer to the heap location.

Image description

In this article, we're focusing on the String datatype in Rust, which is more complex than primitive datatypes such as i32 or f32. Unlike these fixed-size datatypes, a String's size isn't known at compile time, which means it can't be stored on the stack. Consequently, it isn't automatically cleaned up when its owning variable goes out of scope. Therefore, it's crucial to understand how Rust handles the cleanup process for Dynamically allocated variables.

Now let's start with Ownership

Ownership:

In Rust, Every piece of data in Rust has a clear owner responsible for its memory allocation and release. When the owner goes out of scope, the data and its memory are automatically reclaimed, preventing leaks and dangling pointers. Imagine each piece of data having a single, responsible caretaker.
This eliminates manual memory management, as the ownership system handles memory deallocation automatically.

fn main(){
    let s = String::from("hello"); // s is the owner of the string "hello"
    let t = s; // ownership of the value of s is not transferred to t
    println!("{}", s); //gives an error because ownership of is transferred so s does not have any value
    println!("{}", t); // shows hello
}
Enter fullscreen mode Exit fullscreen mode

In Rust, if we want to have multiple borrowers of a single value we have borrowers i.e. borrowing with rules

Lifetimes: Lifetimes define how long references are valid. They ensure that borrowed references don't outlive the data they point to, preventing the dreaded "dangling references".

fn main(){
    loop {
        let x = 5; // x is only valid in this scope
    }
    println!("{}",x); // Unreachable Code
}
Enter fullscreen mode Exit fullscreen mode

References:

In Rust, references allow you to access the data of a variable without taking ownership. Here are some key points:

Immutable References:

By default, references are immutable. This means you can read from a reference, but you can't modify the data it points to.

let s = String::from("hello");
let r = &s; // r is an immutable reference to s
Enter fullscreen mode Exit fullscreen mode

Mutable References:

If you need to modify the data reference points to, you can create a mutable reference using &mut. However, you can only have one mutable reference to a particular piece of data in a particular scope.

fn main(){
    let mut s = String::from("hello");
    let r = &mut s; // r is a mutable reference to s
    *r = String::from("world"); // This changes the value of s
}
Enter fullscreen mode Exit fullscreen mode

Dangling References:

Rust ensures references are always valid. You can't create a dangling reference (a reference to data that no longer exists).

Borrowing:

You can have unlimited borrows of a value as long as they're all immutable. Once something is borrowed as mutable, it can only be borrowed as mutable once and the original owner can't be used until the mutable reference goes out of scope. Borrowing rules ensure safe, conflict-free access, preventing data races and other concurrency nightmares.

fn main(){
    let s = String::from("hello");
    let t = &s; // t borrows s
    println!("{}", s); // This is fine because t is an immutable borrow
}
Enter fullscreen mode Exit fullscreen mode

In conclusion, Rust's memory management model, with its concepts of ownership, borrowing, and lifetimes, is a powerful tool that provides both safety and efficiency. However, this is just the tip of the iceberg. There's much more to explore and understand about Rust's memory model and its implications for concurrent and systems programming. As you delve deeper into Rust, you'll discover how these concepts interplay to provide a unique blend of performance, safety, and zero-cost abstractions. Keep exploring, keep learning, and enjoy the journey into the fascinating world of Rust.

Top comments (0)