A smart pointer is a special type of data structure that behaves like a regular pointer. it stores the memory address of some data, but it also does much more than that.
Smart pointers come with built-in features like:
- Automatic memory management.
- Ownership tracking.
- Borrowing enforcement at runtime.
In simple terms, a pointer is just a reference to some data in memory. But a smart pointer is a pointer with “intelligence”, and it knows how to manage that data safely and efficiently.
Why Use Smart Pointers In Rust?
Rust’s ownership and borrowing rules are powerful; they help prevent memory bugs, data races, and undefined behavior. But sometimes, you need more control over memory managed, shared, or modified. For these problems, we used smart pointers.
- Automatic Memory Management: One of the biggest advantages of smart pointers is that they automatically manage memory for you. They allocate memory when needed and clean it up automatically when the data is no longer in use
- Safe Data Sharing: Smart pointers like Rc<T> (Reference Counted) and Arc<T> (Atomic Reference Counted) allow you to share ownership safely by keeping track of how many references exist.
- Mutability Control: Smart pointers like RefCell<T> allow using interior mutability; you can mutate data safely at runtime while still following Rust’s borrowing rules.
- Advanced Data Structures: You can design powerful, memory-safe, and efficient data structures without giving up Rust’s safety guarantees.
Types of Smart Pointers in Rust
- Box<T>
- Rc<T>
- RefCell<T>
- Arc<T>
- Weak<T>
1) Box<T>: Heap Allocation
In Rust, when you create variables normally, they are usually stored on the stack, a small, fast memory area. But sometimes you want to store big data or data that needs a fixed memory address.
Box<T> is a smart pointer that moves your data from the stack to the heap. It still gives you ownership of that data, but the data now lives in the heap instead of the stack.
Example: Using Box
fn main() {
// Normally, this would store 42 on the stack.
// But using Box::new(), we store it on the heap.
let number = Box::new(42);
println!("Number inside the box: {}", number);
// Ownership: The Box 'owns' the data now.
// When 'number' goes out of scope, the heap memory is automatically freed.
}
Key Points:
- Box::new(42) allocates the integer 42 on the heap.
- number is a smart pointer that points to that heap memory.
2) Rc<T>: Shared Ownership
Rc<T> (short for Reference Counted) is a smart pointer that allows multiple variables to share ownership of the same data on the heap.
Normally in Rust, only one variable can own a piece of data. But if you use Rc<T>, many variables can “own” the same data at the same time, only for reading (it’s not mutable by default).
Example: Using Rc
use std::rc::Rc;
fn main() {
// Create data on the heap with Rc
let msg = Rc::new(String::from("Rust is powerful!"));
// Share ownership with multiple variables
let user1 = Rc::clone(&msg);
let user2 = Rc::clone(&msg);
println!("Message for user1: {}", user1);
println!("Message for user2: {}", user2);
println!("Message original: {}", msg);
// Show how many references (owners) exist
println!("Current reference count: {}", Rc::strong_count(&msg));
{
let user3 = Rc::clone(&msg);
println!("Added one more owner (user3). Count: {}", Rc::strong_count(&msg));
} // user3 goes out of scope here → count decreases
println!("After user3 is dropped. Count: {}", Rc::strong_count(&msg));
}
3) RefCell<T>: Runtime Borrow Checking
RefCell<T> is a smart pointer that allows you to mutably borrow data even if it’s immutable. The borrowing rules are checked at runtime (while the program runs) instead of compile time (when the code is checked by the compiler).
Example: Using RefCell
use std::cell::RefCell;
fn main() {
// Create a RefCell containing a number
let number = RefCell::new(100);
// Mutably borrow the data at runtime
{
let mut val = number.borrow_mut();
*val += 50; // Modify the value
println!("Value inside mutable borrow: {}", val);
} // mutable borrow ends here
// Now borrow immutably
let read_val = number.borrow();
println!("Final value after mutation: {}", read_val);
// Uncommenting the code below will cause a panic:
// let _a = number.borrow_mut();
// let _b = number.borrow_mut(); // runtime panic (two mutable borrows)
}
Key Points:
- Borrowing rules are checked at runtime instead of compile time.
- Panics if borrowing rules are violated.
4) Arc<T>: Thread-Safe Shared Ownership
Arc<T> is a smart pointer in Rust that allows multiple threads to safely share ownership of the same piece of data.
It is very similar to Rc<T> (Reference Counted), but with one major difference: Arc<T> is atomic, meaning it uses atomic operations internally to update the reference count safely across threads.
Example: Using Arc
use std::sync::Arc;
use std::thread;
fn main() {
// Create shared data on the heap wrapped in Arc
let numbers = Arc::new(vec![10, 20, 30]);
// Spawn multiple threads, each getting a clone of the Arc
let mut handles = vec![];
for i in 0..3 {
let shared_numbers = Arc::clone(&numbers);
let handle = thread::spawn(move || {
println!("Thread {i} sees: {:?}", shared_numbers);
});
handles.push(handle);
}
// Wait for all threads to complete
for handle in handles {
handle.join().unwrap();
}
println!("Main thread also sees: {:?}", numbers);
}
5) Weak<T>: Non-Owning References
Weak<T> is a special type of smart pointer that references data managed by an Rc<T> or Arc<T> without increasing the reference count.
Think of it as a temporary observer that can look at the data but doesn’t claim ownership.
Example: Using Weak
use std::rc::{Rc, Weak};
fn main() {
// Create a strong Rc pointer
let strong_ptr = Rc::new(String::from("Hello, Rust!"));
// Create a Weak pointer (non-owning)
let weak_ptr: Weak<String> = Rc::downgrade(&strong_ptr);
println!("Strong count: {}", Rc::strong_count(&strong_ptr)); // 1
println!("Weak count: {}", Rc::weak_count(&strong_ptr)); // 1
// Upgrade Weak pointer to access the value
if let Some(upgraded) = weak_ptr.upgrade() {
println!("Weak pointer upgraded: {}", upgraded);
} else {
println!("The value has been dropped!");
}
// Drop the strong pointer
drop(strong_ptr);
// Now upgrading will fail
if let Some(upgraded) = weak_ptr.upgrade() {
println!("Still exists: {}", upgraded);
} else {
println!("Value no longer exists after strong pointer is dropped.");
}
}
Explanation:
- Rc::new(…) creates a strong owning reference to the data on the heap.
- Rc::downgrade(&strong_ptr) creates a weak pointer that observes the data without owning it.
Learn Other Topics About Rust
- What are threads in Rust?
- What is concurrency in Rust?
- What is error handling in Rust?
- What is an HashMap in Rust?
- What is an Iterator in Rust?
- What is Mutex in Rust?
- What is Async in Rust?

M.Sc. (Information Technology). I explain AI, AGI, Programming and future technologies in simple language. Founder of BoxOfLearn.com.