Skip to content

Wiki

Smart pointer

About 18 min read

Existing English translations were reused. 2 current source blocks remain in Simplified Chinese, without triggering paid translation.

1.What problem does this section solve?

new/deleteYou can manually manage dynamic memory, but it requires you to remember to release resources on all paths. Once a function returns early,return throws an exception, or multiple objects hold pointers to each other, memory leaks, double frees, and dangling pointers easily occur.

Smart pointers encode "who is responsible for freeing the object" into the type:

Smart pointers Ownership semantics Typical scenarios
std::unique_ptr<T> Exclusive ownership Default choice; an object has only one owner
std::shared_ptr<T> Shared ownership The object genuinely needs multiple owners to jointly extend its lifetime
std::weak_ptr<T> Weak reference, non-owning Observe the object managed by shared_ptr, commonly used to break cyclic references

Smart pointers are essentially RAII: when a smart pointer object goes out of scope, its destructor automatically releases the managed object.

2.C++ Standard Edition

  • std::unique_ptr:C++11
  • std::shared_ptr / std::weak_ptr:C++11
  • std::make_unique:C++14
  • std::make_shared:C++11

The required header file is <memory>.

3.Learning Sequence

  1. First look at the problem with manual new/delete on early return.
  2. Use unique_ptr to solve single ownership and automatic release.
  3. Use std::move to transfer unique_ptr ownership.
  4. Only use shared_ptr when ownership is genuinely shared.
  5. Use weak_ptr to observe objects and break reference cycles.
  6. Function parameters should express true semantics: borrowing, transfer, and sharing are three different things.

4.Example code

4.1.Example 1: Problems with the old approach and RAII in unique_ptr

This example deliberately causes the function to return early. In the old style, delete was not executed; in the unique_ptr version, destruction occurs automatically even if the function returns early.

#include <iostream>
#include <memory>
#include <string>

class Resource
{
    std::string name_;

public:
    explicit Resource(const std::string& name) : name_(name)
    {
        std::cout << name_ << " created\n";
    }

    ~Resource()
    {
        std::cout << name_ << " destroyed\n";
    }

    void use() const
    {
        std::cout << name_ << " used\n";
    }
};

bool old_style()
{
    Resource* res = new Resource("old resource");
    res->use();

    std::cout << "old_style: early return\n";
    return false; // res 没有 delete,析构函数不会执行
}

bool modern_style()
{
    auto res = std::make_unique<Resource>("modern resource");
    res->use();

    std::cout << "modern_style: early return\n";
    return false; // res 离开作用域,自动 delete
}

int main()
{
    std::cout << std::boolalpha;

    bool old_ok = old_style();
    std::cout << "old_ok = " << old_ok << "\n";

    std::cout << "---\n";

    bool modern_ok = modern_style();
    std::cout << "modern_ok = " << modern_ok << "\n";

    return 0;
}

Running result:

old resource created
old resource used
old_style: early return
old_ok = false
---
modern resource created
modern resource used
modern_style: early return
modern resource destroyed
modern_ok = false

Note that the first output does not contain old resource destroyed, which is the most common error when manually adding new/delete in complex paths.

4.2.Example 2: Exclusive Ownership and Transfer of unique_ptr

unique_ptrIt cannot be copied, only moved. After moving, the original pointer becomes a null pointer.

#include <iostream>
#include <memory>
#include <string>
#include <utility>

class Student
{
    std::string name_;

public:
    explicit Student(const std::string& name) : name_(name)
    {
        std::cout << "Student " << name_ << " created\n";
    }

    ~Student()
    {
        std::cout << "Student " << name_ << " destroyed\n";
    }

    const std::string& name() const
    {
        return name_;
    }
};

// 智能指针负责管理对象生命周期,减少手动释放资源的风险。
void take_ownership(std::unique_ptr<Student> student)
{
    std::cout << "take ownership of " << student->name() << "\n";
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    auto p = std::make_unique<Student>("Alice");

    auto owner = std::move(p);
    std::cout << "p is null? " << (p == nullptr ? "yes" : "no") << "\n";
    std::cout << "owner has " << owner->name() << "\n";

    take_ownership(std::move(owner));
    std::cout << "owner is null? " << (owner == nullptr ? "yes" : "no") << "\n";

    return 0;
}

Running result:

Student Alice created
p is null? yes
owner has Alice
take ownership of Alice
Student Alice destroyed
owner is null? yes

4.3.Example 3: Shared ownership of shared_ptr

shared_ptrManage objects through reference counting. Only after the last shared_ptr is destroyed or reset does the object get released.

#include <iostream>
#include <memory>
#include <string>

class File
{
    std::string name_;

public:
    explicit File(const std::string& name) : name_(name)
    {
        std::cout << "File " << name_ << " opened\n";
    }

    ~File()
    {
        std::cout << "File " << name_ << " closed\n";
    }

    void read() const
    {
        std::cout << "read from " << name_ << "\n";
    }
};

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // 智能指针负责管理对象生命周期,减少手动释放资源的风险。
    auto file1 = std::make_shared<File>("log.txt");
    std::cout << "count after create = " << file1.use_count() << "\n";

    {
        auto file2 = file1;
        std::cout << "count in scope = " << file1.use_count() << "\n";
        file2->read();
    }

    std::cout << "count after scope = " << file1.use_count() << "\n";

    file1.reset();
    std::cout << "file1 is empty? " << (file1 == nullptr ? "yes" : "no") << "\n";

    return 0;
}

Running result:

File log.txt opened
count after create = 1
count in scope = 2
read from log.txt
count after scope = 1
File log.txt closed
file1 is empty? yes

shared_ptr 很方便,但不是"更安全的默认指针"。它有引用计数开销,也会让所有权关系变复杂。默认优先考虑 unique_ptr。

4.4.Example 4: weak_ptr Breaking shared_ptr circular references

If two objects point to each other with shared_ptr, their reference counts may never drop to zero. A common approach is: use shared_ptr for the primary direction, and weak_ptr for the reverse observation relationship.

#include <iostream>
#include <memory>
#include <string>

class Node
{
public:
    std::string name;
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;

    explicit Node(const std::string& n) : name(n)
    {
        std::cout << "Node " << name << " created\n";
    }

    ~Node()
    {
        std::cout << "Node " << name << " destroyed\n";
    }
};

int main()
{
    auto a = std::make_shared<Node>("A");
    auto b = std::make_shared<Node>("B");

    a->next = b; // A 拥有下一个节点
    b->prev = a; // B 只观察前一个节点,不增加 A 的引用计数

    std::cout << "a count = " << a.use_count() << "\n";
    std::cout << "b count = " << b.use_count() << "\n";

    if (auto p = b->prev.lock())
    {
        std::cout << "B prev is " << p->name << "\n";
    }

    return 0;
}

Running result:

Node A created
Node B created
a count = 1
b count = 2
B prev is A
Node A destroyed
Node B destroyed

weak_ptr cannot be accessed directly; you need to call lock() first. If the object is still alive, lock() returns a temporary shared_ptr; if the object has already been released, it returns a null pointer.

4.4.1.What relationship does this example express?

This code can be viewed as a doubly linked list with only two nodes:

A  ----next---->  B
A  <---prev-----  B

Although both nodes can find each other, theownership implications are not the same in the two directions:

  • A through next owns B; therefore, next use std::shared_ptr<Node>.
  • B through prev observation A but is not responsible for extending the lifetime of A; therefore, prev use std::weak_ptr<Node>.

It can be simply understood as:

  • shared_ptr Means "I own this object; it cannot be destroyed for now."
  • weak_ptr Means "I know where this object is, but I am not responsible for keeping it alive."

4.4.2.How does the reference count change?

When two nodes are first created:

auto a = std::make_shared<Node>("A");
auto b = std::make_shared<Node>("B");

At this time a has A,b has B:

object has its shared_ptr reference count
A a 1
B b 1

Execute:

a->next = b;

a->next is also a shared_ptr, which together with b shares B,so the reference count of B changes from 1 to 2.

Then execute:

b->prev = a;

b->prev is weak_ptr, only observes A, and does not increment the reference count. Therefore, the final output is:

a count = 1
b count = 2

The correspondence is:

object has itsshared_ptr reference count
A a 1
B b,a->next 2

4.4.3.Why can't the reverse pointer also use shared_ptr

Assume the member is changed to:

std::shared_ptr<Node> prev;

Thus A through next owns B. B and through prev owns A:

A  ----shared_ptr---->  B
A  <---shared_ptr----  B

When main() ends, the local variables a and b are destroyed, but the two nodes still hold references to each other internally:

A 的引用计数至少为 1:来自 B.prev
B 的引用计数至少为 1:来自 A.next

它们的引用计数都无法降到 0,因此两个对象都不会析构,这就是 shared_ptr 的循环引用问题。

Using weak_ptr as a back pointer, B.prev does not participate in the reference count of A. The circular ownership is broken, and the object can be released normally.

4.4.4.What is the function of lock()?

weak_ptrdoes not own the object, so it cannot guarantee that the object it observes still exists. For this reason, one cannot directly write:

// 错误:weak_ptr 不支持直接使用 -> 访问对象
std::cout << b->prev->name << "\n";

Must be called before access:

b->prev.lock()

lock() will attempt to obtain a weak_ptr based on shared_ptr:

  • If the object still exists, a valid shared_ptr is returned.
  • If the object has been destroyed, an empty shared_ptr is returned.

Therefore, the original code is written as:

if (auto p = b->prev.lock())
{
    std::cout << "B prev is " << p->name << "\n";
}

This code can be understood in three steps:

  1. b->prev.lock() Try to obtain a pointer to A's shared_ptr.
  2. Save the return value into the local variable p.
  3. if Check whether p is non-null, and enter the code block only if the object still exists.

In this example,A is still managed by the variable a, so lock() succeeds,p points to A, and can be safely accessed:

p->name

During the lifetime of p , it also temporarily holds A , so the reference count of A temporarily changes from 1 to 2. After leaving the if code block, p is destroyed, and the reference count changes from 2 back to 1.

4.4.5.What happens when lock() fails?

In the following example,A after being destroyed,weak_ptr itself can still exist, but it is already in an expired state:

std::weak_ptr<Node> observer;

{
    auto owner = std::make_shared<Node>("A");
    observer = owner;
} // owner 销毁,A 的引用计数变成 0,A 被释放

if (auto p = observer.lock())
{
    std::cout << p->name << "\n";
}
else
{
    std::cout << "observed object has been destroyed\n";
}

At this point observer.lock() returns empty shared_ptr; the program enters else and does not access objects that have already been destroyed.

You can also first use expired() to determine:

if (!observer.expired())
{
    // 对象可能仍然存在
}

However, when actual access to the object is needed, one should still directly use lock() and check the return value. Because between the call to expired() and the call to lock() the object's lifetime may still change; lock() can combine "checking whether the object exists" and "temporarily acquiring ownership" into a single safe operation.

4.4.6.Why can both nodes be destructed when the program ends?

main() At the end, local variables are destroyed in the reverse order of their creation, so b is destroyed first, then a:

  1. bAfter destruction,B's reference count changes from 2 to 1 because a->next still owns B.
  2. aAfter destruction,A's reference count changes from 1 to 0, so A is destructed.
  3. AWhen is destroyed, its member next is also destroyed, and B's reference count changes from 1 to 0.
  4. Finally, B is destructed.

So in the end, it can be seen that:

Node A destroyed
Node B destroyed

The most critical point here is not "which type of smart pointer must be used for the forward and backward pointers respectively," but rather to first determine the ownership relationship:The party truly responsible for the object's lifetime uses shared_ptr, while the party only responsible for observation uses weak_ptr.

4.5.Example 5: Do not abuse function parameters shared_ptr

Function parameters should express true semantics:

  • Just temporary use of an object: use T& or const T&.
  • Function may not have an object: use T* or const T*.
  • Function needs to take over an object: use std::unique_ptr<T>.
  • Function needs to co-own an object: use std::shared_ptr<T>.
#include <iostream>
#include <memory>
#include <string>
#include <utility>

class Robot
{
    std::string name_;

public:
    explicit Robot(std::string name) : name_(std::move(name)) {}

    const std::string& name() const
    {
        return name_;
    }
};

void print_robot(const Robot& robot)
{
    std::cout << "borrow: " << robot.name() << "\n";
}

// 智能指针负责管理对象生命周期,减少手动释放资源的风险。
void take_robot(std::unique_ptr<Robot> robot)
{
    std::cout << "take: " << robot->name() << "\n";
}

void share_robot(std::shared_ptr<Robot> robot)
{
    std::cout << "share count inside = " << robot.use_count() << "\n";
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    auto robot = std::make_unique<Robot>("R1");
    print_robot(*robot);
    take_robot(std::move(robot));
    std::cout << "robot is null? " << (robot == nullptr ? "yes" : "no") << "\n";

    auto shared_robot = std::make_shared<Robot>("R2");
    std::cout << "share count before = " << shared_robot.use_count() << "\n";
    share_robot(shared_robot);
    std::cout << "share count after = " << shared_robot.use_count() << "\n";

    return 0;
}

Running result:

borrow: R1
take: R1
robot is null? yes
share count before = 1
share count inside = 2
share count after = 1

5.Key syntax explanation

Syntax Meaning Notes
std::make_unique<T>(...) Create an exclusively owned object C++ Available since 14
std::move(p) Transfer ownership of unique_ptr out After the move, the original pointer is usually null
std::make_shared<T>(...) Create a shared-ownership object Reference counting has overhead
sp.use_count() View the shared reference count Usable for teaching and debugging; do not rely on it in business logic
std::weak_ptr<T> Weak reference; does not increase the reference count Before access, must lock()
p.reset() Release the currently managed object For shared_ptr, this decrements the reference count by one

6.Common Errors

  1. Copying unique_ptr as a plain pointer.unique_ptr can only be moved, not copied.
  2. Although it is only borrowing the object, the parameter is written as shared_ptr. This implies that the function also participates in shared ownership.
  3. Use one shared_ptr raw pointer to create another shared_ptr. This produces two control blocks and can eventually lead to double free.
  4. Two objects each hold the other with shared_ptr, causing a circular reference.
  5. Use a smart pointer to manage a stack object. For example, pass the address of a local variable to unique_ptr; when it leaves the scope, it will incorrectly free stack memory.

7.Usage Recommendations

  1. Prefer not to use pointers if you can avoid them; prefer ordinary objects and references.
  2. When dynamic ownership is needed, prefer unique_ptr.
  3. Use shared_ptr only when there really are multiple owners.
  4. When only observing an object managed by shared_ptr, use weak_ptr.
  5. To create smart pointers, prefer make_unique and make_shared.
  6. Function parameters should express semantics: do not mix borrowing, transfer, and sharing.

8.Summary

  • Smart pointers are a typical application of RAII.
  • unique_ptr It expresses exclusive ownership and is the default first choice.
  • shared_ptr It expresses shared ownership; do not abuse it just because it is convenient.
  • weak_ptr It does not own the object and is often used to resolve circular references.
  • The most important thing about smart pointers is not "automatic delete," but making ownership relationships clear.