第 18.9 節

Smart pointer

0瀏覽次數0訪問次數--跳出率--平均停留

What problem does this section solve?

new/delete allows manual management of dynamic memory, but it requires you to remember to release resources on all code paths. Once a function goes wrong early, throws an exception, or multiple objects hold pointers to each other, it becomes easy to encounter memory leaks, double-free errors, or dangling pointers.

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

Smart pointermeaning of ownershipTypical scenarios
std::unique_ptr<T>Sole ownershipDefault selection, an object has only one owner.
std::shared_ptr<T>Shared ownershipObjects indeed require multiple owners to collectively extend their lifetime.
std::weak_ptr<T>weak references do not ownObserve the managed object of shared_ptr, commonly used to break circular references.

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

C++ standard version

  • 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>.

Learning Sequence

  1. First, examine the manual new/delete for issues when returning early.
  2. Use unique_ptr to solve single ownership and automatic release.
  3. Use std::move to transfer ownership of unique_ptr.
  4. Only use shared_ptr when there is indeed shared ownership.
  5. Use weak_ptr to observe objects and break circular references.
  6. Function parameters must express true semantics: borrowing, transferring, and sharing are three different things.

Example code

Example 1: Issues with Old Approaches and the RAII Principle of unique_ptr

This example deliberately makes the function return early. In the old version, delete is not executed; in the unique_ptr version, even if the function returns early, the object will automatically destruct.

#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;
}

Results

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 in the first paragraph output, there is no old resource destroyed, and this is the most common error that occurs with manual new/delete under complex paths.

Example 2: unique_ptr's exclusive ownership and transfer

unique_ptr cannot be copied, only moved. After being moved, 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;
}

Results

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

Example 3: Shared Ownership with shared_ptr

shared_ptr manages objects using reference counting. The object is released only after the last shared_ptr is destroyed or reassigned.

#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;
}

Results

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 is very convenient, but not "a safer default pointer". It has reference counting overhead and will also complicate ownership relationships. By default, prioritize unique_ptr.

Example 4: Using weak_ptr to break circular references of shared_ptr

If two objects reference each other using shared_ptr, their reference counts may never reach zero. A common practice is to 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;
}

Results

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 directly access the object; you must first call lock(). If the object is still alive, lock() returns a temporary shared_ptr; if the object has already been released, it returns a null pointer.

Example 5: Don't overuse shared_ptr as function parameters

Function parameters should express true semantics:

  • For temporary use of objects: use T& or const T&.
  • Functions might not have objects: use T* or const T*.
  • The function should take over the object: using std::unique_ptr<T>.
  • Functions need to share objects: 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;
}

Results

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

Key Grammar Explanation

writing methodMeaningPrecautions
std::make_unique<T>(...)Create exclusive ownership objectsAvailable since C++14
std::move(p)Transfer the ownership of unique_ptr.The original pointer usually becomes null after being moved.
std::make_shared<T>(...)Create shared ownership objectsReference counting has overhead.
sp.use_count()View shared reference countFor teaching and debugging purposes, but business logic should not depend on it.
std::weak_ptr<T>Weak reference, does not increment the reference countAccess requires lock()
p.reset()Release the currently managed object.For shared_ptr, it decrements the reference count by one.

Common Errors

  1. Treat unique_ptr as an ordinary pointer and copy it. unique_ptr can only be moved, not copied.
  2. The parameter is written as shared_ptr, which is clearly just a borrowed object. This implies that the function also participates in shared ownership.
  3. Create another shared_ptr from a raw pointer of shared_ptr. This results in two control blocks, which may eventually lead to duplicate releases.
  4. Two objects hold references to each other using shared_ptr, resulting in circular references.
  5. Using smart pointers to manage stack objects is problematic. For instance, if the address of a local variable is passed to unique_ptr, it will incorrectly deallocate the stack memory when leaving the scope.

使用建议

  • 明确目标:在开始前确定您的具体需求,以便选择最合适的工具或教程。
  • 充分利用资源:参考官方文档、教程和博客,这些资料能帮助您快速上手并解决问题。
  • 实践应用:通过动手操作项目或编写代码来巩固学习成果,提升实际操作能力。
  • 问题解决:遇到困难时,查阅参考资料或寻求社区支持,逐步培养独立解决问题的能力。
  • 分享经验:完成项目后,可以撰写文章或博客分享心得,帮助其他学习者。

如果需要针对特定领域(如单片机、机器人或环境搭建)的进一步建议,请提供更多信息,我将为您细化内容。

  1. Avoid using pointers when possible; prioritize regular objects and references.
  2. When dynamic ownership is needed, prioritize using unique_ptr.
  3. Only use shared_ptr when there are indeed multiple owners.
  4. When only observing objects managed by shared_ptr, use weak_ptr.
  5. When creating smart pointers, prefer make_unique and make_shared.
  6. Function parameters must convey semantics: borrowing, transferring, and sharing should not be mixed.

Summary

  • Smart pointers are a typical application of RAII.
  • unique_ptr conveys exclusive ownership and is the default and preferred choice.
  • shared_ptr expresses shared ownership, don't overuse it just for convenience.
  • weak_ptr does not own objects, and is often used to resolve circular references.
  • The most important thing about smart pointers isn't "automatic delete," but clearly defining the ownership relationships.
音乐页