Rust: Rust Smart Pointers
Last updated: 2026-08-26
A smart pointer is a "data structure with additional behavior"—it references memory just like a pointer, but offers capabilities beyond those of a regular pointer, such as automatic memory management, reference counting, and borrow checking.
If an ordinary pointer is like “a piece of paper with an address written on it,” then a smart pointer is like “a property management system that comes with its own security guards, janitors, and ledger.” Rust’s smart pointers guarantee safety at compile time and have no runtime overhead (aside from the minimal overhead of reference counting).
1. What You'll Learn
Box<T>Heap Allocation and Ownership TransferDereftrait—Overloading the dereference operatorDroptrait—Automatic cleanup via destructorsRc<T>Reference Counting—Single-Threaded Multi-Owner SharingRefCell<T>Internal Variability—Runtime Borrowing ChecksRc<RefCell<T>>Combination Mode—Shared + Editable
2. The Story of Shared Desks
(1) Problem: Multiple users want to view and edit the same document at the same time
On Monday morning, Luna's company implemented a "shared desk" policy. There were only three desks for a team of five people.
- Alice needs to look at a project proposal (PDF), but she doesn't want to print a copy herself—she wants to look at the one someone else has.
- Bob needs to review the same proposal as well, and he needs to make notes on it.
- Carol wants to view it at the same time, but only read it—not edit it.
- The problem is: if Alice finishes reading the document and destroys it, but Bob and Carol are still reading it—that’s when things go wrong.
- To make matters worse, while Bob was making changes, Alice was making changes too—so the page got messed up.
"If only we knew how many people are still looking at this document, we could just destroy it once no one is looking at it anymore..." "If only we could ensure that when one person is editing it, no one else can make changes..."
(2) Rust Smart Pointer Solutions
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// Box: One person has exclusive access to a document,Destroy after reading
let doc_box = Box::new(String::from("Project Proposal v1"));
println!("Box Hold: {}", doc_box);
// doc_box Automatically destroyed upon leaving the scope——No manual intervention required free
// Rc: Shared Read-Only Access for Multiple Users
let doc_rc = Rc::new(String::from("Shared Project Proposal"));
let alice = Rc::clone(&doc_rc);
let bob = Rc::clone(&doc_rc);
println!("Reference Count: {}", Rc::strong_count(&doc_rc)); // 3
// Rc<RefCell<T>>: Share + Variable
let doc_shared = Rc::new(RefCell::new(String::from("Collaborative Documents")));
let alice_view = Rc::clone(&doc_shared);
let bob_view = Rc::clone(&doc_shared);
// Bob Add annotations to a document(Needs to be revised)
bob_view.borrow_mut().push_str("\nBob Comments on:Part 2 needs to be completed");
// Alice View the documentation(Read-only)
println!("Alice See: {}", alice_view.borrow());
// Carol See the documentation as well(Read-only)
println!("Carol See: {}", doc_shared.borrow());
}
Rust uses three types of smart pointers to address thread-safety issues related to "sharing" and "modification":
Boxexclusive ownership and heap allocation,Rcshared read-only ownership, andRefCellinternal mutability achieved through runtime checks of borrowing rules.
3. Core Concepts
(1) Smart Pointer System
graph TB
A[Rust Smart Pointers] --> B[Box<T> Heap Allocation]
A --> C[Rc<T> Reference Count]
A --> D[RefCell<T> Internal Variability]
B --> B1["let b = Box::new(42)"]
B --> B2["Automatically leaves scope drop"]
C --> C1["Rc::clone Increase the reference count"]
C --> C2["strong_count == 0 Destroy on time"]
C --> C3["Use in a single-threaded environment"]
D --> D1["borrow() → Immutable References"]
D --> D2["borrow_mut() → Variable References"]
D --> D3["Runtime Check of Borrowing Rules"]
A --> E[Combination Mode]
E --> E1["Rc<RefCell<T>>"]
E --> E2["Shared Ownership + Internal Variability"]
A --> F[Core Trait]
F --> F1["Deref: Dereference Operator *"]
F --> F2["Drop: Destructor"]
(2) Comparison of Three Types of Smart Needles
| Property | Box<T> | Rc<T> | RefCell<T> |
|---|---|---|---|
| Ownership | Single ownership | Shared ownership (reference counting) | Single ownership |
| Mutability | Mutable (Box::new followed by *b = val) |
Immutable (read-only shared) | Internally mutable (checked at runtime) |
| Check Timing | Compile Time | Compile Time | Runtime |
| Performance Overhead | No additional overhead | Reference count changes (minimal) | Runtime borrow checks (minimal) |
| Thread-safe | Yes | No (single-threaded) | No (single-threaded) |
| Use Cases | Heap-allocated large datasets, recursive types | Multi-path shared read-only data | Need to modify data under immutable references |
(3) Deref and Drop traits
| Trait | Method | Function |
|---|---|---|
Deref |
fn deref(&self) -> &T |
Allow *x dereferencing operations to apply to custom types |
Drop |
fn drop(&mut self) |
Cleanup logic automatically called when a value leaves its scope |
(4) Comparison of Box, Rc, and Arc Models
| Characteristics | Box<T> |
Rc<T> |
Arc<T> |
|---|---|---|---|
| Ownership | Single owner | Multiple owners (reference counting) | Multiple owners (atomic reference counting) |
| Thread-safe | No (single-threaded only) | No (single-threaded only) | Yes (can be shared across threads) |
| Reference Count | None | Non-atomic, low overhead | Atomic operation, slightly higher overhead |
| Variable access | &mut Directly variable |
Requires RefCell |
Requires Mutex / RwLock |
| Typical Scenarios | Recursive Types / Big Data Heap Allocation | DAG / Shared Read-Only Data | Multithreaded Data Sharing |
| Performance | Fastest | Average | Slightly slower (atomic operations) |
DerefMakes smart pointers work just like regular references—Box<T>can be used just like&T.Dropensures that resources are automatically released—no need to manuallyfreeordelete.
4. Smart Pointer Examples
▶ Example 1: Box—Heap Allocation and Recursive Types (Difficulty ⭐)
Output:
MyBox It was destroyed.~
Box the value in: <b>
Recursive List: <list>
*y == <*y>
Greeting: <name>
// ============================================
// Box<T> Three Typical Uses of:Heap Allocation、Recursive Types、Deref
// ============================================
// Recursive Types:Cons list(Must use Box,Because the size is unknown at compile time)
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
// Custom Smart Pointers (Demo Deref and Drop)
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0 // A reference to the internal value
}
}
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
// In practical applications, resources are released here(For example, closing a file、Release the network connection)
// println!("MyBox It was destroyed.~");
}
}
fn main() {
// --- 1. Box Basic Heap Allocation ---
let b = Box::new(42);
println!("Box the value in: {}", b); // Automatic Dereferencing
// --- 2. Recursive Types:Cons List ---
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println!("Recursive List: {:?}", list);
// --- 3. Custom MyBox + Deref ---
let my_box = MyBox::new(String::from("Hello"));
// Deref lets &MyBox<String> automatically convert to &String, then to &str
greet(&my_box);
// --- 4. Dereference Operator ---
let x = 10;
let y = MyBox::new(x);
assert_eq!(10, *y); // *y Equivalent to *(y.deref())
println!("*y == {}", *y);
}
fn greet(name: &str) {
println!("Greeting: {}", name);
}
Output:
Box the value in: 42
Recursive List: Cons(1, Cons(2, Cons(3, Nil)))
Greeting: Hello
*y == 10
The core value of
Box<T>is twofold: first, it places data on the heap (retaining only pointers on the stack); second, it allows types whose size is unknown at compile time (such as recursive types) to be used normally. TheDereftrait enablesBox<T>to be dereferenced just like&T—this is where the “smart” in “smart pointer” comes from.
▶ Example 2: Rc—Reference Counting with Shared Ownership (Difficulty ⭐⭐)
Output:
Initial reference count: <Rc::strong_count(&book_c)>
Alice Reference Count After Borrowing: <Rc::strong_count(&book_c)>
Bob Reference Count After Borrowing: <Rc::strong_count(&book_c)>
Alice Book List: <*alice>
Bob Book List: <*bob>
Bob Citation Count After Returning the Book: <Rc::strong_count(&book_c)>
Alice Still reading: <*alice>
// ============================================
// Rc<T> Reference Count:Multipath Sharing of Read-Only Data
// ============================================
use std::rc::Rc;
#[derive(Debug)]
enum BookList {
Cons(String, Rc<BookList>),
Nil,
}
use BookList::{Cons, Nil};
fn main() {
// --- Scene:Three books are shared by two readers ---
// Create a Basic Reading List
let book_c = Rc::new(Cons("Rust Programming".to_string(),
Rc::new(Cons("Introduction to Algorithms".to_string(),
Rc::new(Cons("Design Patterns".to_string(),
Rc::new(Nil))))));
println!("Initial reference count: {}", Rc::strong_count(&book_c));
// Alice Borrowed the reading list(Rc::clone Increase the reference count only,Do not deep-copy data)
let alice = Rc::clone(&book_c);
println!("Alice Reference Count After Borrowing: {}", Rc::strong_count(&book_c));
{
// Bob I also borrowed the reading list(In the inner scope)
let bob = Rc::clone(&book_c);
println!("Bob Reference Count After Borrowing: {}", Rc::strong_count(&book_c));
// Both of them can read it.
println!("Alice Book List: {:?}", *alice);
println!("Bob Book List: {:?}", *bob);
} // Bob Out of scope,Decrease in reference count
println!("Bob Citation Count After Returning the Book: {}", Rc::strong_count(&book_c));
// Alice Still reading
println!("Alice Still reading: {:?}", *alice);
// All Rc After they have all gone out of scope,Only then is the data truly destroyed
// Rc::strong_count == 0 When triggered drop
}
Output:
Initial reference count: 1
Alice Reference Count After Borrowing: 2
Bob Reference Count After Borrowing: 3
Alice Book List: Cons("Rust Programming", Cons("Introduction to Algorithms", Cons("Design Patterns", Nil)))
Bob Book List: Cons("Rust Programming", Cons("Introduction to Algorithms", Cons("Design Patterns", Nil)))
Bob Citation Count After Returning the Book: 2
Alice Still reading: Cons("Rust Programming", Cons("Introduction to Algorithms", Cons("Design Patterns", Nil)))
Rc<T>stands for "Reference Counted."Rc::clone(&x)does not perform a deep copy of the data; it simply increments the reference count by 1. The data is destroyed only when allRchandles go out of scope (and the reference count reaches zero).Rccan only be used in a single-threaded environment—for multithreading, useArc<T>.
▶ Example 3: RefCell—Internal Variability (Difficulty ⭐⭐)
Output:
Log #<i + 1>: <msg>
<ref1[0]>
RefCell The presentation is over.
// ============================================
// RefCell<T>:Runtime Borrowing Checks + Internal Variability
// ============================================
use std::cell::RefCell;
// Simulate a "Log Entry" Messenger
// External code can only obtain immutable references, but the log needs to be modified internally.
trait Messenger {
fn send(&self, msg: &str);
}
// Logger:For internal use only RefCell Store Messages
struct Logger {
// Even if Logger Inherently immutable,messages It can still be edited
messages: RefCell<Vec<String>>,
}
impl Logger {
fn new() -> Logger {
Logger {
messages: RefCell::new(Vec::new()),
}
}
}
impl Messenger for Logger {
fn send(&self, msg: &str) {
// borrow_mut() gets a mutable reference — even if self is &self
self.messages.borrow_mut().push(msg.to_string());
}
}
fn main() {
let logger = Logger::new();
// Calling via an immutable reference send——But it has actually been modified internally.
logger.send("User Login");
logger.send("Click the button");
logger.send("Data submitted successfully");
// borrow() Get an immutable reference
let msgs = logger.messages.borrow();
for (i, msg) in msgs.iter().enumerate() {
println!("Log #{}: {}", i + 1, msg);
}
// --- Examples of Runtime Borrowing Violations(Cancel the Commentary Session panic)---
// let mut_ref = logger.messages.borrow_mut();
// let ref1 = logger.messages.borrow(); // panic! Both mutable and immutable references
// println!("{}", ref1[0]);
println!("RefCell The presentation is over.");
}
Output:
Log #1: User Login
Log #2: Click the button
Log #3: Data submitted successfully
RefCell The presentation is over.
The core of
RefCell<T>is "interior mutability": even ifLoggeritself is an immutable reference to&self,RefCellstill allows you to modify the internal data. The difference is that borrowing rules are not checked at compile time, but rather at runtime—if a rule is violated, the program will panic instead of generating a compile-time error.
▶ Example 4: Rc<RefCell<T>>—Shared + Mutable (Difficulty ⭐⭐⭐)
Output:
Current reference count: <Rc::strong_count(&board)>
=== Whiteboard Content ===
<board.read()>
Alice Length of content viewed: <alice_board.read().len()>
Bob Length of content viewed: <bob_board.read().len()>
Carol Length of content viewed: <carol_board.read().len()>
// ============================================
// Rc<RefCell<T>> Combination Mode:Shared Ownership + Internal Variability
// ============================================
use std::rc::Rc;
use std::cell::RefCell;
// Simulate a"Shared Whiteboard"——Team members can write and read
#[derive(Debug)]
struct Whiteboard {
content: RefCell<String>,
}
impl Whiteboard {
fn new() -> Whiteboard {
Whiteboard {
content: RefCell::new(String::new()),
}
}
fn write(&self, text: &str) {
let mut content = self.content.borrow_mut();
content.push_str(text);
content.push('\n');
}
fn read(&self) -> String {
self.content.borrow().clone()
}
}
fn main() {
// Create a shared whiteboard, wrapped with Rc so it can be held by multiple people
let board = Rc::new(Whiteboard::new());
// Alice and Bob both hold references to the whiteboard
let alice_board = Rc::clone(&board);
let bob_board = Rc::clone(&board);
let carol_board = Rc::clone(&board);
println!("Current reference count: {}", Rc::strong_count(&board)); // 4
// Alice Write on the whiteboard
alice_board.write("Alice: Today's Discussion Rust Smart Pointers");
// Bob Additional Information
bob_board.write("Bob: I'll explain the difference between Box and Rc");
// Carol Write as well
carol_board.write("Carol: RefCell Runtime borrowing checks are important");
// Everyone can view the full content(Because they share the same RefCell)
println!("=== Whiteboard Content ===");
println!("{}", board.read());
// Verify that all references point to the same data
println!("Alice Length of content viewed: {}", alice_board.read().len());
println!("Bob Length of content viewed: {}", bob_board.read().len());
println!("Carol Length of content viewed: {}", carol_board.read().len());
// board When leaving the scope,Reference Count Reset to Zero,Whiteboard Automatic Destruction
}
Output:
Current reference count: 4
=== Whiteboard Content ===
Alice: Today's Discussion Rust Smart Pointers
Bob: I'll explain the difference between Box and Rc
Carol: RefCell Runtime borrowing checks are important
Alice Length of content viewed: 73
Bob Length of content viewed: 73
Carol Length of content viewed: 73
Rc<RefCell<T>>is one of the most powerful composable patterns in single-threaded Rust programming:Rcsolves the problem of “multiple people wanting to hold” something, whileRefCellsolves the problem of “people holding immutable references also wanting to modify” it. If we compareRcto a “shared library card,” thenRefCellis a “special copy that allows annotations” .
▶ Example 5: Comprehensive Exercise—Graph Data Structures (Difficulty ⭐⭐⭐)
Output:
=== Graph Structure ===
<node.borrow()>
=== Nodes Reachable from Starting Point A ===
A's first neighbor: <neighbor.borrow().value>
Take it a step further from that neighbor: <second_hop>
=== Strong reference counting ===
A's Rc reference count: <Rc::strong_count(&a)>
After clone, A's Rc reference count: <Rc::strong_count(&a)>
After drop, A's Rc reference count: <Rc::strong_count(&a)>
// ============================================
// Comprehensive Example:Rc<RefCell<T>> Graph Node Implementation
// ============================================
use std::rc::{Rc, Weak};
use std::cell::RefCell;
use std::fmt;
struct Node {
value: String,
neighbors: Vec<Weak<RefCell<Node>>>,
}
impl Node {
fn new(value: &str) -> Rc<RefCell<Node>> {
Rc::new(RefCell::new(Node {
value: value.to_string(),
neighbors: Vec::new(),
}))
}
fn add_neighbor(node: &Rc<RefCell<Node>>, neighbor: &Rc<RefCell<Node>>) {
node.borrow_mut().neighbors.push(Rc::downgrade(neighbor));
}
}
impl fmt::Debug for Node {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let neighbor_names: Vec<String> = self.neighbors.iter()
.filter_map(|w| w.upgrade())
.map(|n| n.borrow().value.clone())
.collect();
write!(f, "{} -> {:?}", self.value, neighbor_names)
}
}
fn main() {
let a = Node::new("A");
let b = Node::new("B");
let c = Node::new("C");
let d = Node::new("D");
Node::add_neighbor(&a, &b);
Node::add_neighbor(&a, &c);
Node::add_neighbor(&b, &c);
Node::add_neighbor(&b, &d);
Node::add_neighbor(&c, &d);
println!("=== Graph Structure ===");
for node in [&a, &b, &c, &d] {
println!("{:?}", node.borrow());
}
println!("\n=== Nodes Reachable from Starting Point A ===");
if let Some(neighbor) = a.borrow().neighbors.first().and_then(|w| w.upgrade()) {
println!("A's first neighbor: {}", neighbor.borrow().value);
let second_hop: Vec<String> = neighbor.borrow().neighbors.iter()
.filter_map(|w| w.upgrade())
.map(|n| n.borrow().value.clone())
.collect();
println!("Take it a step further from that neighbor: {:?}", second_hop);
}
println!("\n=== Strong reference counting ===");
println!("A's Rc reference count: {}", Rc::strong_count(&a));
let a_clone = Rc::clone(&a);
println!("After clone, A's Rc reference count: {}", Rc::strong_count(&a));
drop(a_clone);
println!("After drop, A's Rc reference count: {}", Rc::strong_count(&a));
}
Output:
=== Graph Structure ===
A -> ["B", "C"]
B -> ["C", "D"]
C -> ["D"]
D -> []
=== Nodes Reachable from Starting Point A ===
A's first neighbor: B
Take it a step further from that neighbor: ["C", "D"]
=== Strong reference counting ===
A's Rc reference count: 1
After clone, A's Rc reference count: 2
After drop, A's Rc reference count: 1
Graph nodes use
Rc<RefCell<Node>>to implement shared ownership and internal mutability; neighbor relationships useWeak<RefCell<Node>>to prevent memory leaks caused by circular references.Rc::downgradecreates a weak reference, andweak.upgrade()attempts to upgrade it to a strong reference.
❓ FAQ
Box<T> and a regular reference &T?Box<T> owns the data, while &T merely borrows it.Rc<T> and Arc<T>?Rc<T> is single-threaded reference counting, while Arc<T> is multi-threaded atomic reference counting.RefCell<T> and Cell<T>?Cell<T> implements internal mutability through value copying (or moving), while RefCell<T> implements it through references.RefCell<T> instead of &mut T?RefCell when you hold an immutable reference but need to modify the data.Rc<RefCell<T>> and &mut T?Rc<RefCell<T>> incurs overhead from reference counting and runtime borrowing checks, while &mut T has zero overhead.📖 Summary
Box<T>is a heap-allocated smart pointer used for recursive types, large data sets, and trait objects—it ensures unique ownership with zero additional overhead.Dereftrait enables smart pointers to support the*dereference operator, while theDroptrait ensures that resources are automatically cleaned up when the scope endsRc<T>is a reference-counted smart pointer that enables shared ownership in a single-threaded environment—Rc::cloneonly increments the count without copying the dataRefCell<T>Provides internal mutability, deferring borrow checks from compile time to runtime—rule violations cause a panicCell<T>Implement internal mutability using value copying (requiresT: Copy);RefCell<T>implements it using references (any type allowed)Rc<RefCell<T>>Combination mode = shared ownership + internal mutability, is the most commonly used flexible data-sharing solution in Rust single-threaded programming
📝 Exercises
- Difficulty ⭐: Write a program that uses
Box<T>to create a recursiveCons List(containing at least 3 elements), and then prints the entire list. You must use#[derive(Debug)]to print the list. - Difficulty ⭐⭐: Implement a "shared note" system: Use
Rc<RefCell<String>>to allow three people (Alice, Bob, and Carol) to share a single note string. Everyone can read it, and everyone can write to it (append content). Verify that everyone can see the complete content after all have finished writing. - Difficulty ⭐⭐⭐: Implement a "Graph Observer" pattern. Define
trait Observer { fn notify(&self, msg: &str); }, then implementLoggerObserver(which usesRefCell<Vec<String>>internally to log messages). Next, useRc<RefCell<dyn Observer>>to allow multiple observers to be notified by the same subject. In themainfunction, create two observers; have the subject send three messages, and verify that both observers received all messages.