We continue our learning of <span>Rust</span> and today we will explore the knowledge related to smart pointers.
This article will provide a simple overview.
We will discuss several of the most commonly used types in the standard library:
- •
<span>Box<T></span>, used for allocating values on the heap - •
<span>Rc<T></span>, a reference-counted type that allows multiple owners of the data - •
<span>Ref<T></span>and<span>RefMut<T></span>, which are accessed through<span>RefCell<T></span>. The<span>RefCell<T></span>type enforces borrowing rules at runtime rather than at compile time.
Additionally, we will discuss the interior mutability pattern, which allows an immutable type to expose an API that can modify its internal values. We will also discuss how reference cycles can lead to memory leaks and how to avoid them.
Pointer is a general concept that contains the memory address of a variable. This address references, or “points at,” some other data. The most common pointer in Rust is the reference (reference), which is denoted by the <span>&</span> symbol and borrows the value it points to. They have no special functionality beyond referencing data and incur no additional overhead.
In Rust, pointers are divided into two categories: ordinary references (<span>&T</span>) and smart pointers. Ordinary references are lightweight “borrowed pointers” that only point to data without owning it; while smart pointers are complex data structures with metadata and additional functionality that not only point to data like pointers but also own the data and implement flexible behavior through the <span>Deref</span> and <span>Drop</span> traits.
1. Pointers vs Smart Pointers: Core Differences
To understand smart pointers, it is essential to clarify their core differences from ordinary references (a type of pointer):
| Feature | Ordinary Reference (<span>&T</span>/<span>&mut T</span>) |
Smart Pointer (e.g., <span>Box<T></span>) |
| Ownership | None (only borrows) | Has (usually owns the pointed data) |
| Additional Functionality | None (only points to data) | Has (metadata, automatic release, counting, etc.) |
| Core Trait | No additional implementation required | Must implement <span>Deref</span> and <span>Drop</span> |
| Overhead | Zero overhead | Some additional overhead (metadata storage) |
| Use Case | Short-term borrowing, no ownership transfer | Heap allocation, multiple owners, runtime borrowing checks |
In simple terms:Ordinary references are “minimal pointers” that pursue zero overhead and safe borrowing; smart pointers are “enhanced pointers” that pursue ownership management, additional functionality, and scenario adaptation..
2. Core Traits of Smart Pointers: <span>Deref</span> and <span>Drop</span> Traits
All smart pointers rely on two core traits to implement “pointer behavior” and “lifecycle management,” which is key to distinguishing them from ordinary structs:
1. <span>Deref</span> Trait: Makes Smart Pointers “Work Like References”
<span>Deref</span> trait allows instances of smart pointer structs to overload the dereference operator (<span>*</span>), making them behave like ordinary references. This means you can use code that handles ordinary references seamlessly with smart pointers without distinguishing the types.
For example, <span>Box<T></span> implements <span>Deref</span>, so it can be dereferenced like an ordinary reference:
let b = Box::new(5);
println!("{}", *b); // Outputs 5, no special syntax needed, just like dereferencing &5
This ability to “simulate references” makes the code more generic — for instance, a function that takes a <span>&i32</span> parameter can accept both ordinary references and <span>Box<i32></span> (through automatic conversion via <span>Deref</span>).
2. <span>Drop</span> Trait: Custom Resource Release Logic
<span>Drop</span> trait allows us to define the “cleanup logic” when a smart pointer goes out of scope, such as releasing heap memory, closing file handles, or disconnecting network connections. Rust automatically calls the <span>drop</span> method, eliminating the need for manual resource management and preventing memory leaks.
All smart pointers implement <span>Drop</span>:
- •
<span>Box<T></span>will release the memory on the heap when it goes out of scope; - •
<span>Rc<T></span>will release the data when the reference count reaches zero; - • Custom smart pointers can implement
<span>Drop</span>for complex resource automatic cleanup.
3. Common Smart Pointers in the Standard Library: Scenario-Based Selection
The Rust standard library provides three commonly used smart pointers, each suited for different scenarios, covering core needs such as “heap allocation,” “multiple owners,” and “runtime borrowing checks.”
1. <span>Box<T></span>: A “Simple Smart Pointer” Allocated on the Heap
Core Purpose: To allocate data on the heap, retaining only a pointer to the heap data on the stack.
Use Cases:
- • Storing types with unknown size at compile time (e.g., recursive types);
- • Avoiding stack copies when passing large data (only storing a pointer on the stack, which incurs low overhead);
- • Implementing dynamic polymorphism as trait objects.
Key Features:
- • Exclusive ownership (a
<span>Box<T></span>uniquely owns the heap data); - • Zero additional overhead (only stores the data pointer, no other metadata);
- • Automatic release (calls
<span>Drop</span>to release heap memory when going out of scope).
// Allocating an i32 on the heap, stack variable b is a pointer to the heap data
let b = Box::new(10);
println!("Value on the heap: {}", *b); // Dereference, outputs 10
// b goes out of scope, automatically releasing heap memory
2. <span>Rc<T></span>: A “Reference Counted Pointer” Supporting Multiple Owners
Core Purpose: To allow data to have multiple owners, tracking the number of owners through reference counting, and releasing data when the count reaches zero.
Use Cases:
- • Read-only data that needs to be shared among multiple parts (e.g., configuration information, immutable caches);
- • Circular data structures (to avoid circular references, must be used with
<span>Weak<T></span>).
Key Features:
- • Shared ownership (multiple
<span>Rc<T></span>point to the same heap data); - • Read-only access (default does not support modification, must be used with
<span>RefCell<T></span><span> to achieve interior mutability);</span> - • Reference counting is not thread-safe (only suitable for single-threaded scenarios, which we will discuss later in thread pools).
use std::rc::Rc;
let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a); // Reference count +1 (no data copy, only pointer copy)
let c = Rc::clone(&a); // Reference count +1
println!("Reference count: {}", Rc::strong_count(&a)); // Outputs 3
// a, b, c go out of scope in turn, count reaches 0, string is released
3. <span>RefCell<T></span> + <span>Ref<T>/RefMut<T></span>: A “Mutable Smart Pointer” with Runtime Borrowing Checks
Core Purpose: To implement “interior mutability” in scenarios where compile-time borrowing rules cannot be satisfied, allowing modification of internal data of an immutable <span>RefCell<T></span> instance.
Use Cases:
- • Immutable objects need to modify internal state (e.g., cache updates, observer patterns);
- • Compile-time cannot prove borrowing safety, but runtime can guarantee it (e.g., internal modifications of complex data structures).
Key Features:
- • Interior mutability (
<span>&RefCell<T></span><span> can obtain mutable references through </span><code><span>borrow_mut()</span><span>);</span> - • Runtime borrowing checks (panic when rules are violated, rather than compile-time errors);
- • Single-threaded use (not thread-safe, for multi-threading use
<span>Mutex<T></span><span>);</span> - • Access permissions are managed through
<span>Ref<T></span><span> (immutable borrow) and </span><code><span>RefMut<T></span><span> (mutable borrow), following the “one mutable reference or multiple immutable references” rule.</span>
use std::cell::RefCell;
let cell = RefCell::new(5);
{
let mut mutable_ref = cell.borrow_mut(); // Runtime obtains mutable reference
*mutable_ref = 10;
} // mutable_ref goes out of scope, releasing mutable reference
let immutable_ref = cell.borrow(); // Obtain immutable reference
println!("Internal value: {}", *immutable_ref); // Outputs 10
4. Key Concept: Interior Mutability Pattern
<span>RefCell<T></span><span> implements “interior mutability,” an important pattern in Rust — it allows a </span><strong><span>externally immutable</span></strong><span> type to expose a mutable interface internally. The core principle is:</span>
- • The struct itself is immutable (
<span>&T</span><span>), but internally contains a </span><code><span>RefCell<T></span><span>);</span> - • Through the
<span>RefCell<T></span><span>’s </span><code><span>borrow()</span><span> and </span><code><span>borrow_mut()</span><span> methods, borrowing rules are checked at runtime, allowing internal data modification;</span> - • This pattern balances the safety of “externally immutable” with the flexibility of “interior mutability,” commonly found in immutable objects that need to maintain state (e.g., loggers, configuration managers).
5. Risks to Note: Reference Cycles and Memory Leaks
While smart pointers are beneficial, improper use can lead to reference cycles — when two or more <span>Rc<T></span><span> reference each other, causing the reference count to never reach zero, preventing data from being released and resulting in memory leaks.</span>
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
next: Option<rc<refcell>> // Points to another Node
}
let a = Rc::new(RefCell::new(Node { next: None }));
let b = Rc::new(RefCell::new(Node { next: Some(Rc::clone(&a)) }));
a.borrow_mut().next = Some(Rc::clone(&b)); // a references b, b references a, forming a cycle</rc<refcell
The solution is to use <span>Weak<T></span><span> (a weak reference of </span><code><span>Rc<T></span><span>):</span><code><span>Weak<T></span><span> does not increase the reference count and serves as a “non-owning reference,” suitable for breaking reference cycles.</span>
Conclusion: Smart Pointer Selection Guide
The core value of Rust smart pointers is to “provide flexible ownership management and data access methods under the premise of safety.” When choosing, you can follow these principles:
- • Need heap allocation and exclusive ownership → Use
<span>Box<T></span><span>;</span> - • Need multiple owners and read-only access → Use
<span>Rc<T></span><span>;</span> - • Need interior mutability (modifying internal state of immutable objects) → Use
<span>RefCell<T></span><span> (single-threaded)/</span><code><span>Mutex<T></span><span> (multi-threaded);</span> - • Need to break
<span>Rc<T></span><span> reference cycles → Use </span><code><span>Weak<T></span><span>.</span>
Smart pointers are a key design in Rust that balances “safety” and “flexibility.” Mastering them will enable you to handle complex scenarios (such as recursive types, shared states, dynamic polymorphism) while avoiding memory leaks and data races.