Memory Safety Explained Through Real Rust Examples


Introduction
Rust is a modern systems programming language designed for performance, safety, and concurrency. It offers low-level control like C and C++, but without the risk of undefined behavior or memory corruption. Rust achieves this through a strict ownership model and powerful compile-time checks — making it ideal for writing reliable software where performance and safety matter most. That’s why it's rapidly gaining popularity in areas like embedded systems, operating systems, web servers, and even game engines. But how? That’s exactly what we’re here to explore.
What is memory safety and why is matters?
Memory safety means that a program only accesses memory it's allowed to, and in valid ways.
In C/C++, it's easy to:
Use memory after it's freed
Modify memory via a null or uninitialized pointer
Access arrays out of bounds
These bugs compile without warning but crash at runtime — usually with a segmentation fault.
Preventing them requires knowing exactly where every pointer points to, when it's freed, and how it interacts with the rest of your code. That’s a lot to track manually — and humans are bad at that. Rust, on the other hand, helps the compiler track it for you.
How Rust handles memory: Ownership
Rust uses a unique way to handle memory and ensure that the program is memory safe during compile time(without a garbage collector) called ownership.
Ownership is a set of rules that the compiler checks, if any of the rules are violated, the program won’t compile. These rules are as follow:
Each value in Rust has an owner.
There can only be one owner at a time.
When the owner goes out of scope, the value will be dropped.
Let’s take an example to better illustrate it.
fn main() {
let s = String::from("hello");
let t = s; // s is moved
println!("{}", s); // ❌ compile error
}
In this example, s
is the owner of a heap-allocated string. When we assign s
to t
, ownership is moved — s
is no longer valid. Attempting to use s
after the move results in a compile-time error.
This rule prevents bugs like use-after-free, and ensures only one part of your program manages a given piece of memory at a time.
fn main() {
let s = String::from("hello"); // Scope starts
println!("{}", s); // Scope ends
}
When s
is declared here this is the start of the scope. The line where s
is printed is last line where s
is used to it’s the end of the scope. After the scope end, s will be freed automatically, since it’s no longer needed.
Borrowing and Referencing
Once you understand ownership, a common question arises: “How do I use a variable without taking ownership of it?”
There are two main approaches: cloning and borrowing. Cloning
Cloning makes a complete copy of the data and its memory:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println("{} {}", s1, s2);
}
This works, but duplicates both the pointer and the underlying data in memory — which can be inefficient in large applications.
Borrowing
Borrowing lets you use a value without taking ownership. You pass a reference instead of the actual value:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
In the preceding example, we where trying to know the length of the string s1
using the custom function calculate_length
, which in turn uses the function len()
. If we try to pass s1
itself as parameter, the variable s
will take ownership, which will result in a compilation error when trying to print s1
.
To prevent this issue we will have to borrow the value of s1
in let len = calculate_length(&s1);
Note that &
is the symbol used for borrowing.
Mutable References
So far, all references we’ve seen are immutable — you can read from them, but not modify the value. What if you want to change the value via a reference?
Rust allows this using mutable references, but only if:
The original variable is declared
mut
You pass it using a mutable reference
&mut
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
This works because:
s
is mutable&mut s
gives a mutable referencesome_string
is allowed to change the data
Rust enforces strict rules here: you can have either one mutable reference or any number of immutable references at a time — but never both. This prevents data races and undefined behavior, even in single-threaded code.
Lifetimes (Without the Headache)
Rust’s borrowing rules guarantee memory safety, but what if a function returns a reference? How does the compiler know that the reference will still be valid after the function ends?
That’s where lifetimes come in.
A lifetime tells the compiler:
“This reference must be valid for at least as long as this.”
Let’s look at an example:
fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
This function returns the longer of two string slices. The compiler needs to ensure that the returned reference doesn't outlive either x
or y
. The 'a
tells Rust that:
Both
x
andy
must live at least as long as'a
, andThe return value also lives at least as long as
'a
This guarantees safety: the returned reference won’t outlive the inputs, and thus won’t dangle.
Why This Matters
In C/C++, you can return a pointer to a local variable and compile just fine — only to face undefined behavior at runtime. Rust makes this impossible by enforcing lifetime rules at compile time.
Most of the time, you don’t have to write lifetimes manually — the compiler can infer them. But when returning references or working with complex borrowing logic, lifetimes help you express intent clearly and catch mistakes early.
Final Thoughts
Rust enforces memory safety without a garbage collector — using ownership, borrowing, and lifetimes. It might feel strict at first, but these rules eliminate an entire class of bugs that plague C/C++ codebases.
If your Rust code compiles, chances are it works — and won’t crash in production due to memory errors.
With Rust being a powerful language with low level control and many application starting from system programming or embedded systems to backend development, learning it opens the door for many opportunities and in different fields. Rust is what I would consider a high investment high reward.
Subscribe to my newsletter
Read articles from Ahmed Elshentenawy directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
