第 20.9 節

智能指针

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

本节解决什么问题

new/delete 能手动管理动态内存,但它要求你在所有路径上都记得释放资源。一旦函数提前 return、抛异常,或者多个对象互相持有指针,就很容易出现内存泄漏、重复释放、悬空指针。

智能指针把"谁负责释放对象"写进类型里:

智能指针所有权含义典型场景
std::unique_ptr<T>独占所有权默认选择,一个对象只有一个拥有者
std::shared_ptr<T>共享所有权对象确实需要多个拥有者共同延长生命周期
std::weak_ptr<T>弱引用,不拥有观察 shared_ptr 管理的对象,常用于打破循环引用

智能指针本质上是 RAII:智能指针对象离开作用域时,它的析构函数会自动释放所管理的对象。

C++ 标准版本

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

需要的头文件是 <memory>

学习顺序

  1. 先看手动 new/delete 在提前返回时的问题。
  2. unique_ptr 解决单一所有权和自动释放。
  3. std::move 转移 unique_ptr 所有权。
  4. 只有确实共享所有权时,才用 shared_ptr
  5. weak_ptr 观察对象并打破循环引用。
  6. 函数参数要表达真实语义:借用、转移、共享是三件不同的事。

示例代码

示例 1:旧写法的问题和 unique_ptr 的 RAII

这个例子故意让函数提前返回。旧写法中,delete 没有执行;unique_ptr 版本中,即使提前返回也会自动析构。

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

运行结果

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

注意第一段输出里没有 old resource destroyed,这就是手动 new/delete 在复杂路径下最容易出的错。

示例 2:unique_ptr 的独占所有权和转移

unique_ptr 不能复制,只能移动。移动之后,原来的指针会变成空指针。

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

运行结果

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

示例 3:shared_ptr 的共享所有权

shared_ptr 通过引用计数管理对象。最后一个 shared_ptr 销毁或 reset 后,对象才会释放。

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

运行结果

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:weak_ptr 打破 shared_ptr 循环引用

如果两个对象互相用 shared_ptr 指向对方,它们的引用计数可能永远不会归零。常见做法是:主要方向用 shared_ptr,反向观察关系用 weak_ptr

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

运行结果

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

weak_ptr 不能直接访问对象,需要先调用 lock()。如果对象还活着,lock() 返回一个临时 shared_ptr;如果对象已经释放,返回空指针。

这个例子表达了什么关系

这段代码可以看成一个只有两个节点的双向链表:

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

虽然两个节点都能找到对方,但两个方向的所有权含义并不相同

  • A 通过 next 拥有 B,因此 next 使用 std::shared_ptr<Node>
  • B 通过 prev 观察 A,但不负责延长 A 的生命周期,因此 prev 使用 std::weak_ptr<Node>

可以把它简单理解为:

  • shared_ptr 表示“我拥有这个对象,它暂时不能被销毁”。
  • weak_ptr 表示“我知道这个对象在哪里,但我不负责让它继续存活”。

引用计数是怎样变化的

刚创建两个节点时:

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

此时 a 拥有 Ab 拥有 B

对象拥有它的 shared_ptr引用计数
Aa1
Bb1

执行:

a->next = b;

a->next 也是一个 shared_ptr,它和 b 共同拥有 B,所以 B 的引用计数由 1 变成 2。

接着执行:

b->prev = a;

b->prevweak_ptr,只观察 A,不会增加引用计数。因此最终输出为:

a count = 1
b count = 2

对应关系是:

对象拥有它的 shared_ptr引用计数
Aa1
Bba->next2

为什么反向指针不能也使用 shared_ptr

假设把成员改成:

std::shared_ptr<Node> prev;

那么 A 通过 next 拥有 BB 又通过 prev 拥有 A

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

main() 结束时,局部变量 ab 虽然会销毁,但两个节点内部仍然互相持有:

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

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

使用 weak_ptr 作为反向指针后,B.prev 不参与 A 的引用计数,循环所有权被打破,对象便可以正常释放。

lock() 的作用是什么

weak_ptr 不拥有对象,因此它不能保证自己观察的对象仍然存在。也正因为如此,不能直接写:

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

访问前必须调用:

b->prev.lock()

lock() 会尝试根据 weak_ptr 得到一个 shared_ptr

  • 如果对象仍然存在,返回一个有效的 shared_ptr
  • 如果对象已经销毁,返回一个空的 shared_ptr

因此原代码写成:

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

这段代码可以分成三步理解:

  1. b->prev.lock() 尝试获得指向 Ashared_ptr
  2. 返回值保存到局部变量 p 中。
  3. if 检查 p 是否非空,只有对象仍然存在时才进入代码块。

在这个例子中,A 仍然由变量 a 管理,所以 lock() 成功,p 指向 A,可以安全访问:

p->name

p 存活期间,它也会暂时拥有 A,因此 A 的引用计数会临时从 1 变成 2。离开 if 代码块后,p 被销毁,引用计数再从 2 变回 1。

lock() 失败时会怎样

下面的例子中,A 被销毁以后,weak_ptr 本身仍然可以存在,但它已经处于过期状态:

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

这时 observer.lock() 返回空的 shared_ptr,程序进入 else,不会访问已经销毁的对象。

也可以先使用 expired() 判断:

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

不过需要真正访问对象时,仍然应该直接使用 lock() 并检查返回值。因为从调用 expired() 到调用 lock() 之间,对象的生命周期仍可能发生变化;lock() 能把“检查对象是否存在”和“临时取得所有权”合并为一次安全操作。

程序结束时为什么两个节点都能析构

main() 结束时,局部变量按照创建顺序的相反方向销毁,因此先销毁 b,再销毁 a

  1. b 销毁后,B 的引用计数从 2 变成 1,因为 a->next 仍然拥有 B
  2. a 销毁后,A 的引用计数从 1 变成 0,于是 A 被析构。
  3. A 被销毁时,它的成员 next 也随之销毁,B 的引用计数从 1 变成 0。
  4. 最后 B 被析构。

所以最终可以看到:

Node A destroyed
Node B destroyed

这里最关键的不是“前后指针必须分别使用哪一种智能指针”,而是要先判断所有权关系:真正负责对象生命周期的一方使用 shared_ptr,只负责观察的一方使用 weak_ptr

示例 5:函数参数不要滥用 shared_ptr

函数参数应该表达真实语义:

  • 只是临时使用对象:用 T&const T&
  • 函数可能没有对象:用 T*const T*
  • 函数要接管对象:用 std::unique_ptr<T>
  • 函数要共同拥有对象:用 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;
}

运行结果

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

关键语法解释

写法含义注意事项
std::make_unique<T>(...)创建独占所有权对象C++14 起可用
std::move(p)unique_ptr 所有权转出去移动后原指针通常为空
std::make_shared<T>(...)创建共享所有权对象引用计数有开销
sp.use_count()查看共享引用计数教学调试可用,业务逻辑不要依赖它
std::weak_ptr<T>弱引用,不增加引用计数访问前必须 lock()
p.reset()释放当前管理的对象shared_ptr 是减少一次引用计数

常见错误

  1. unique_ptr 当普通指针复制。unique_ptr 只能移动,不能复制。
  2. 明明只是借用对象,却把参数写成 shared_ptr。这会暗示函数也参与共享所有权。
  3. 用一个 shared_ptr 的原始指针创建另一个 shared_ptr。这样会产生两个控制块,最终可能重复释放。
  4. 两个对象互相用 shared_ptr 持有对方,导致循环引用。
  5. 用智能指针管理栈对象。例如把局部变量地址交给 unique_ptr,离开作用域时会错误释放栈内存。

使用建议

  1. 能不用指针就不用指针,普通对象和引用优先。
  2. 需要动态所有权时,优先用 unique_ptr
  3. 只有确实有多个所有者时,才用 shared_ptr
  4. 只观察 shared_ptr 管理的对象时,用 weak_ptr
  5. 创建智能指针优先使用 make_uniquemake_shared
  6. 函数参数要表达语义:借用、转移、共享不要混用。

小结

  • 智能指针是 RAII 的典型应用。
  • unique_ptr 表达独占所有权,是默认首选。
  • shared_ptr 表达共享所有权,不要因为方便就滥用。
  • weak_ptr 不拥有对象,常用于解决循环引用。
  • 智能指针最重要的不是"自动 delete",而是把所有权关系写清楚。
音乐页