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



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.

"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

RUST
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": Box exclusive ownership and heap allocation, Rc shared read-only ownership, and RefCell internal mutability achieved through runtime checks of borrowing rules.



3. Core Concepts

(1) Smart Pointer System

100%
graph TB
    A[Rust Smart Pointers] --> B[Box&lt;T&gt; Heap Allocation]
    A --> C[Rc&lt;T&gt; Reference Count]
    A --> D[RefCell&lt;T&gt; 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&lt;RefCell&lt;T&gt;&gt;"]
    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)

Deref Makes smart pointers work just like regular references—Box<T> can be used just like &T. Drop ensures that resources are automatically released—no need to manually free or delete.



4. Smart Pointer Examples

▶ Example 1: Box—Heap Allocation and Recursive Types (Difficulty ⭐)

Output:

TEXT 📖 Display only
MyBox It was destroyed.~
Box the value in: <b>
Recursive List: <list>
*y == <*y>
Greeting: <name>
RUST
// ============================================
// 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:

TEXT 📖 Display only
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. The Deref trait enables Box<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:

TEXT 📖 Display only
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>
RUST
// ============================================
// 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:

TEXT 📖 Display only
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 all Rc handles go out of scope (and the reference count reaches zero). Rc can only be used in a single-threaded environment—for multithreading, use Arc<T>.


▶ Example 3: RefCell—Internal Variability (Difficulty ⭐⭐)

Output:

TEXT 📖 Display only
Log #<i + 1>: <msg>
<ref1[0]>
RefCell The presentation is over.
RUST
// ============================================
// 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:

TEXT 📖 Display only
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 if Logger itself is an immutable reference to &self, RefCell still 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:

TEXT 📖 Display only
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()>
RUST
// ============================================
// 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:

TEXT 📖 Display only
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: Rc solves the problem of “multiple people wanting to hold” something, while RefCell solves the problem of “people holding immutable references also wanting to modify” it. If we compare Rc to a “shared library card,” then RefCell is a “special copy that allows annotations” .


▶ Example 5: Comprehensive Exercise—Graph Data Structures (Difficulty ⭐⭐⭐)

Output:

TEXT 📖 Display only
=== 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)>
RUST
// ============================================
// 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:

TEXT 📖 Display only
=== 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 use Weak<RefCell<Node>> to prevent memory leaks caused by circular references. Rc::downgrade creates a weak reference, and weak.upgrade() attempts to upgrade it to a strong reference.


❓ FAQ

Q What is the difference between Box<T> and a regular reference &T?
A Box<T> owns the data, while &T merely borrows it.
Q What is the difference between Rc<T> and Arc<T>?
A Rc<T> is single-threaded reference counting, while Arc<T> is multi-threaded atomic reference counting.
Q What is the difference between RefCell<T> and Cell<T>?
A Cell<T> implements internal mutability through value copying (or moving), while RefCell<T> implements it through references.
Q When should you use RefCell<T> instead of &mut T?
A Use RefCell when you hold an immutable reference but need to modify the data.
Q What is the performance difference between Rc<RefCell<T>> and &mut T?
A Rc<RefCell<T>> incurs overhead from reference counting and runtime borrowing checks, while &mut T has zero overhead.

📖 Summary


📝 Exercises

  1. Difficulty ⭐: Write a program that uses Box<T> to create a recursive Cons List (containing at least 3 elements), and then prints the entire list. You must use #[derive(Debug)] to print the list.
  2. 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.
  3. Difficulty ⭐⭐⭐: Implement a "Graph Observer" pattern. Define trait Observer { fn notify(&self, msg: &str); }, then implement LoggerObserver (which uses RefCell<Vec<String>> internally to log messages). Next, use Rc<RefCell<dyn Observer>> to allow multiple observers to be notified by the same subject. In the main function, create two observers; have the subject send three messages, and verify that both observers received all messages.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏