C++ Smart Pointers: Types, Usage, and Practices

In today’s article, we will revisit smart pointers in C++.

In resource management in C++, manually using <span>new</span>/<span>delete</span> can easily lead to memory leaks, dangling pointers, and exception safety issues. The smart pointers introduced in C++11 encapsulate ordinary pointers with RAII (Resource Acquisition Is Initialization): automatically releasing resources when the object goes out of scope, making programs safer and easier to maintain.

1. Types of Smart Pointers

The standard library <span><memory></span> provides three core types of smart pointers:

Type Ownership Model Characteristics Typical Scenarios
<span>std::unique_ptr</span> Exclusive Uniquely owns the resource, movable but not copyable Exclusive resource management (files, sockets, mutexes)
<span>std::shared_ptr</span> Shared Multiple pointers share ownership, reference counting Shared caches, plugin systems
<span>std::weak_ptr</span> Observer Does not own the resource, only observes <span>shared_ptr</span> Breaking circular references, weak reference caches

The earlier <span>std::auto_ptr</span> has been deprecated in C++11 and is no longer used.

2. std::unique_ptr — Exclusive Ownership

<span>unique_ptr</span> represents a pointer that uniquely owns the resource. Characteristics:

  • Copying is prohibited, it can only be moved (<span>std::move</span>).
  • Automatically calls <span>delete</span> or a custom deleter when the scope ends.

Basic Usage

#include <memory>
#include <iostream>

struct Foo 
{
    Foo() { std::cout << "Foo()\n"; }
    ~Foo() { std::cout << "~Foo()\n"; }
};

int main() 
{
    std::unique_ptr<Foo> p1 = std::make_unique<Foo>(); // Recommended way

    /* p1 is null, no longer pointing to the object, p2 goes out of scope, automatically delete */
    std::unique_ptr<Foo> p2 = std::move(p1);           // Transfer ownership
} 

Scenarios

  • RAII Encapsulation: Commonly used for exclusive resources such as file descriptors, sockets, mutexes, and other system resources.
  • Factory Function Returns: Clearly indicates ownership transfer.

3. std::shared_ptr — Reference Counting Shared

<span>shared_ptr</span> allows multiple pointers to share ownership of the same object. It internally maintains a control block that contains:

  • Reference count (thread-safe)
  • Deleter

Basic Usage

#include <memory>
#include <iostream>

struct Foo 
{
    Foo() { std::cout << "Foo()\n"; }
    ~Foo() { std::cout << "~Foo()\n"; }
};

int main() 
{
    auto sp1 = std::make_shared<Foo>();
    std::shared_ptr<Foo> sp2 = sp1;         // Reference count +1
    std::cout << sp1.use_count() << '\n';   // Output 2
    // The last shared_ptr destructor automatically deletes
} 

Circular Reference Scenario

#include<iostream>
#include<memory>

class Test2;

class Test1
{
public:
    std::shared_ptr<Test2> test2_ptr;
    ~Test1() 
    {
        std::cout << "Test1 has been destroyed."<< std::endl;
    }
};

class Test2 
{
public:
    std::shared_ptr<Test1> test1_ptr;
    ~Test2() 
    {
        std::cout << "Test2 has been destroyed."<< std::endl;
    }
};

int main() 
{
    std::shared_ptr<Test1> test1 = std::make_shared<Test1>();
    std::shared_ptr<Test2> test2 = std::make_shared<Test2>();
    test1->test2_ptr = test2; // Test1 references Test2
    test2->test1_ptr = test1; // Test2 references Test1
    // Due to circular references, the destructors of Test1 and Test2 will not be called, leading to memory leaks
    return 0;
}

Typical Applications

  • Multi-module Shared Resources: For example, cache objects, configuration centers.
  • Observer Pattern: Event subscribers hold the same subject object.

Considerations

  • Circular References: Objects referencing each other with <span>shared_ptr</span> will never be released, requiring <span>weak_ptr</span> to resolve.
  • Overhead: Additional control block allocation and atomic operations.

4. std::weak_ptr — Weak Reference Observer

<span>weak_ptr</span> does not own the object, it only serves as an observer of <span>shared_ptr</span>.

  • Does not increase the reference count.
  • Can use <span>lock()</span> to safely obtain a <span>shared_ptr</span>, returning null if the object has been destroyed.

Example

#include <memory>
#include <iostream>

struct Foo 
{
    Foo()
    {
        std::cout<<"Foo()\n";
    } 
    ~Foo()
    {
        std::cout<<"~Foo()\n";
    } 
};

int main() 
{
    auto sp = std::make_shared<Foo>();
    std::weak_ptr<Foo> wp = sp;      // Does not increase reference count
    if (auto p = wp.lock()) 
    {        // Attempt to promote
        std::cout << "Object still exists\n";
    }
} // After sp destructs, wp.lock() returns null

Typical Scenarios

  • Breaking Circular References: In bidirectional associated objects, one side holds a <span>shared_ptr</span>, while the other holds a <span>weak_ptr</span>.
  • Caching/Observer: Only need to check if the object is alive, without affecting its lifecycle.

5. Common Methods and Techniques

  1. Prefer using <span>std::make_unique</span> / <span>std::make_shared</span>

  • Avoid raw <span>new</span>, better exception safety.
  • <span>make_shared</span> allocates the object and control block in one go, more efficient.
  • Custom Deleters are suitable for file handles, database connections:

    std::shared_ptr<FILE> fp(fopen("a.txt","r"), fclose);
    
  • Performance Considerations If sharing is not needed, avoid the atomic counting overhead of <span>shared_ptr</span>.

  • Lifecycle Management Smart pointers are just tools for automatic release; ensure that they are not released too early or too late logically.

  • 6. Application Example: Bidirectional Tree Node

    struct Node 
    {
        int value;
        std::shared_ptr<Node> left;
        std::weak_ptr<Node>   parent; // Prevent circular references
    };
    

    <span>left</span> owns child nodes; <span>parent</span> only observes the parent node, avoiding circular references that lead to leaks.

    Below, we use <span>std::shared_ptr</span> and <span>std::weak_ptr</span> to construct bidirectional (parent-child mutual reference) tree nodes, preventing memory leaks caused by circular references.

    Example

    #include <iostream>
    #include <memory>
    #include <string>
    
    // Bidirectional tree node
    struct Node 
    {
        std::string name;
        std::shared_ptr<Node> left;        // Owns child nodes
        std::shared_ptr<Node> right;       // Owns child nodes
        std::weak_ptr<Node>   parent;      // Only observes the parent node, avoiding circular references
    
        explicit Node(std::string n) : name(std::move(n)) 
        {
            std::cout << "Construct: " << name << "\n";
        }
    
        ~Node() 
        {
            std::cout << "Destruct : " << name << "\n";
        }
    };
    
    // Helper function: establish parent-child relationship
    void setChild(const std::shared_ptr<Node>&amp; parent,
                  const std::shared_ptr<Node>&amp; child,
                  bool isLeft)
    {
        if (isLeft) parent->left  = child;
        else        parent->right = child;
        child->parent = parent;            // weak_ptr points to the parent node
    }
    
    int main() 
    {
        // Create root node
        auto root = std::make_shared<Node>("root");
    
        // Create left and right child nodes and establish relationships
        auto leftChild  = std::make_shared<Node>("left");
        auto rightChild = std::make_shared<Node>("right");
    
        setChild(root, leftChild,  true);
        setChild(root, rightChild, false);
    
        // Access parent node: need to lock first
        if (auto p = leftChild->parent.lock()) 
        {
            std::cout << leftChild->name << "'s parent is " << p->name << "\n";
        }
    
        // After the scope ends, root/left/right will all be automatically destructed
        // Because parent is weak_ptr, it will not form circular references
        return 0;
    }
    

    Run Output

        Construct: root
        Construct: left
        Construct: right
        left's parent is root
        Destruct : left
        Destruct : right
        Destruct : root
    

    As we can see:

    • All nodes are correctly destructed, with no memory leaks.
    • <span>parent</span><span> uses </span><code><span>weak_ptr</span>, even if <span>child</span> points to <span>parent</span>, it does not increase the reference count, thus avoiding circular references.

    Key Points Explanation

    1. The parent pointer must be <span>weak_ptr</span> If the parent pointer is <span>shared_ptr</span>, <span>root</span> and <span>leftChild</span> will hold each other, and the reference count will never reach zero, leading to memory leaks.

    2. Use <span>lock()</span> when accessing the parent node<span>weak_ptr::lock()</span> will return a temporary <span>shared_ptr</span>, allowing safe access when the object exists; if the object has been destroyed, <span>lock()</span> returns a null pointer.

    3. RAII Automatic Management No need for manual <span>delete</span><span>, all resources are automatically released when going out of scope, ensuring exception safety.</span>

    Through this method, we can easily implement bidirectional trees, graph structures, or any object model requiring bidirectional references, while avoiding the most common pitfalls of smart pointers—circular references.

    7. Selection

    Requirement Recommendation
    Exclusive Resource <span>unique_ptr</span>
    Multiple Sharing <span>shared_ptr</span>
    Only Observing <span>weak_ptr</span>

    Rule of Thumb: Use <span>unique_ptr</span> whenever possible instead of <span>shared_ptr</span>.

    Conclusion

    The core of smart pointers is RAII + Ownership Model:

    • Exclusive Ownership:<span>unique_ptr</span>
    • Reference Counting Shared:<span>shared_ptr</span>
    • Weak Observation:<span>weak_ptr</span>

    Choosing smart pointers wisely can greatly reduce the risk of memory leaks, simplify resource management, and write safe, concise, and modern C++ code.

    Reading a hundred times, its meaning will naturally appear. Coding is the same, always fresh.

    C++ Smart Pointers: Types, Usage, and Practices

    END

    Author:YuLinMuRong

    Source:Linux ArmoryCopyright belongs to the original author, please contact for deletion if there is infringement..Recommended ReadingWhy is C++ rarely used in microcontroller development?Xiaomi is really stingy, a single MCU actually handles all functionsUploaded a PCB photo with only 2 lines of silk screen, GPT-5 helped me solve everything!→ Follow for more updates ←

    Leave a Comment