第 18.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++11std::shared_ptr/std::weak_ptr:C++11std::make_unique:C++14std::make_shared:C++11
需要的头文件是 <memory>。
学习顺序
- 先看手动
new/delete在提前返回时的问题。 - 用
unique_ptr解决单一所有权和自动释放。 - 用
std::move转移unique_ptr所有权。 - 只有确实共享所有权时,才用
shared_ptr。 - 用
weak_ptr观察对象并打破循环引用。 - 函数参数要表达真实语义:借用、转移、共享是三件不同的事。
示例代码
示例 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;如果对象已经释放,返回空指针。
示例 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 是减少一次引用计数 |
常见错误
- 把
unique_ptr当普通指针复制。unique_ptr只能移动,不能复制。 - 明明只是借用对象,却把参数写成
shared_ptr。这会暗示函数也参与共享所有权。 - 用一个
shared_ptr的原始指针创建另一个shared_ptr。这样会产生两个控制块,最终可能重复释放。 - 两个对象互相用
shared_ptr持有对方,导致循环引用。 - 用智能指针管理栈对象。例如把局部变量地址交给
unique_ptr,离开作用域时会错误释放栈内存。
使用建议
- 能不用指针就不用指针,普通对象和引用优先。
- 需要动态所有权时,优先用
unique_ptr。 - 只有确实有多个所有者时,才用
shared_ptr。 - 只观察
shared_ptr管理的对象时,用weak_ptr。 - 创建智能指针优先使用
make_unique和make_shared。 - 函数参数要表达语义:借用、转移、共享不要混用。
小结
- 智能指针是 RAII 的典型应用。
unique_ptr表达独占所有权,是默认首选。shared_ptr表达共享所有权,不要因为方便就滥用。weak_ptr不拥有对象,常用于解决循环引用。- 智能指针最重要的不是"自动 delete",而是把所有权关系写清楚。