各位技术同仁,下午好!
欢迎来到今天的讲座。我是您的主讲人,一名在 C++ 世界里摸爬滚打多年的老兵。今天,我们将聚焦一个在现代 C++ 开发中既强大又潜藏风险的话题——shared_ptr 的循环引用。
shared_ptr 是 C++11 引入的智能指针家族中的重要一员,它为我们带来了革命性的自动内存管理能力,极大地简化了复杂对象生命周期的维护。它的出现,让许多 C++ 开发者摆脱了手动 new 和 delete 的繁琐与易错性,使得代码更健壮、更安全。然而,就像任何强大的工具一样,shared_ptr 并非万能,它也有自己的“阿喀琉斯之踵”。这个弱点,就是我们今天要深入探讨的——循环引用。
当两个或多个 shared_ptr 实例相互持有对方的强引用,形成一个封闭的环路时,它们就会陷入一种“锁死”的状态。即使外部已经没有其他引用指向这个环路中的任何对象,这些对象也无法被正确销毁,它们的内存将永远得不到释放,直到程序结束。这就是所谓的内存泄漏,它悄无声息地吞噬着系统资源,导致性能下降、程序崩溃,甚至整个系统的不稳定。
本次讲座的目标是:
- 深入理解
shared_ptr的工作机制,特别是其引用计数原理。 - 详细剖析循环引用产生的根源、典型场景及其危害。
- 介绍如何利用
weak_ptr这一关键工具,优雅地解决循环引用问题。 - 探讨
weak_ptr的适用场景、最佳实践以及智能指针家族的协作。 - 分享一些高级议题和调试技巧,帮助大家在实际项目中更好地驾驭智能指针。
我希望通过今天的分享,能帮助大家更深刻地理解智能指针的精髓,掌握避免和解决循环引用的方法,从而编写出更加高效、稳定和易于维护的 C++ 代码。
shared_ptr 的魅力与机制
在深入循环引用之前,让我们先回顾一下 shared_ptr 的基本原理和它带来的便利。
RAII 原则回顾
shared_ptr 的设计理念根植于 C++ 的核心原则之一:资源获取即初始化 (Resource Acquisition Is Initialization, RAII)。RAII 是一种编程范式,它将资源的生命周期与对象的生命周期绑定。当对象被创建时,它获取资源;当对象被销毁时(无论正常退出还是异常抛出),它的析构函数会自动释放资源。这种机制保证了资源总是能被正确管理,极大地减少了内存泄漏和资源泄漏的风险。
例如,一个简单的文件操作可以这样用 RAII 来实现:
#include <fstream>
#include <iostream>
#include <string>
class FileHandler {
public:
FileHandler(const std::string& filename) {
file_.open(filename);
if (!file_.is_open()) {
throw std::runtime_error("Failed to open file: " + filename);
}
std::cout << "File '" << filename << "' opened." << std::cout;
}
~FileHandler() {
if (file_.is_open()) {
file_.close();
std::cout << "File closed." << std::endl;
}
}
void write(const std::string& data) {
if (file_.is_open()) {
file_ << data << std::endl;
}
}
private:
std::ofstream file_;
};
void processFile() {
try {
FileHandler handler("example.txt");
handler.write("Hello, RAII!");
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
// FileHandler 对象在作用域结束时自动销毁,文件句柄自动关闭
}
int main() {
processFile();
return 0;
}
在这个例子中,FileHandler 对象的生命周期决定了文件资源的生命周期。shared_ptr 将这种 RAII 思想应用到了堆内存的管理上。
shared_ptr 的基本用法
shared_ptr 是一种智能指针,它允许多个指针共享同一个对象的所有权。当最后一个 shared_ptr 离开作用域或被重置时,它所指向的对象就会被自动删除。
创建 shared_ptr:
最推荐的方式是使用 std::make_shared 函数。它能在一个单独的内存分配中完成对象和其控制块的创建,效率更高,并且能避免一些潜在的异常安全问题。
#include <memory>
#include <iostream>
class MyObject {
public:
MyObject() { std::cout << "MyObject constructed!" << std::endl; }
~MyObject() { std::cout << "MyObject destructed!" << std::endl; }
void greet() { std::cout << "Hello from MyObject!" << std::endl; }
};
int main() {
// 推荐方式:使用 make_shared
std::shared_ptr<MyObject> ptr1 = std::make_shared<MyObject>();
ptr1->greet();
// 不推荐但合法的方式:直接构造
// std::shared_ptr<MyObject> ptr2(new MyObject());
return 0;
}
共享所有权:
shared_ptr 的核心特性是共享所有权。通过拷贝构造或拷贝赋值,多个 shared_ptr 可以指向同一个对象。
int main() {
std::shared_ptr<MyObject> ptr1 = std::make_shared<MyObject>();
std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // 1
std::shared_ptr<MyObject> ptr2 = ptr1; // 拷贝构造
std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // 2
std::cout << "ptr2 use_count: " << ptr2.use_count() << std::endl; // 2
{
std::shared_ptr<MyObject> ptr3 = ptr1; // 拷贝构造
std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl; // 3
} // ptr3 离开作用域,引用计数减1
std::cout << "ptr1 use_count after ptr3 scope: " << ptr1.use_count() << std::endl; // 2
ptr1.reset(); // ptr1 放弃所有权,引用计数减1
std::cout << "ptr1 use_count after reset: " << (ptr1 ? ptr1.use_count() : 0) << std::endl; // 0 (ptr1 为空)
std::cout << "ptr2 use_count after ptr1 reset: " << ptr2.use_count() << std::endl; // 1
// 当 ptr2 离开作用域时,引用计数变为0,MyObject 对象被销毁
return 0;
}
从上面的输出可以看到,MyObject 的析构函数只在所有 shared_ptr 都放弃所有权时才被调用。
常用成员函数:
use_count(): 返回当前有多少个shared_ptr共享该对象。get(): 返回原始指针,但请谨慎使用,不要通过原始指针delete对象。reset(): 放弃当前对象的所有权,可以指向新对象或置空。operator*()和operator->(): 解引用和成员访问,与普通指针无异。operator bool(): 判断shared_ptr是否为空。
引用计数机制
shared_ptr 的核心在于其内部维护的引用计数 (Reference Count)。每个 shared_ptr 实例并不仅仅包含一个指向对象的指针,它还关联了一个控制块 (Control Block)。这个控制块通常包含:
- 强引用计数 (Strong Reference Count): 记录有多少个
shared_ptr实例正在管理该对象。当强引用计数归零时,对象会被销毁。 - 弱引用计数 (Weak Reference Count): 记录有多少个
weak_ptr实例正在观测该对象。当弱引用计数和强引用计数都归零时,控制块本身才会被销毁。 - 自定义删除器 (Custom Deleter) (可选): 如果提供了自定义删除器,它会存储在这里。
- 分配器 (Allocator) (可选): 如果提供了自定义分配器,它会存储在这里。
当 shared_ptr 被拷贝或赋值时,强引用计数会增加。当 shared_ptr 离开作用域、被重置或被赋值给另一个对象时,强引用计数会减少。当强引用计数归零时,shared_ptr 会调用对象的析构函数来释放其内存。
控制块的生命周期:
对象的生命周期由强引用计数决定。控制块的生命周期由强引用计数和弱引用计数共同决定。即使对象已被销毁,只要还有 weak_ptr 存在,控制块就会继续存在,以便 weak_ptr 可以查询对象是否仍然存活。
让我们通过一个表格来概括 shared_ptr 的行为:
| 操作 | 强引用计数 | 弱引用计数 |
|---|---|---|
make_shared / shared_ptr(new T) |
1 | 0 |
shared_ptr<T> p2 = p1; |
p1.use_count() + 1 |
不变 |
p1 = p3; (p1指向新对象) |
p1 原指向对象的计数 – 1 |
不变 |
p1.reset(); |
p1 原指向对象的计数 – 1 |
不变 |
shared_ptr 析构 |
-1 | 不变 |
weak_ptr<T> w = p1; |
不变 | +1 |
w.lock(); |
w 指向对象的计数 + 1 (如果未过期) |
不变 |
weak_ptr 析构 |
不变 | -1 |
这个引用计数机制是 shared_ptr 实现自动内存管理的关键,但也是循环引用问题的根源。
循环引用:潜伏的陷阱
现在,我们来到了今天讲座的核心:循环引用。理解它为什么会发生以及它会带来什么后果,是解决问题的第一步。
什么是循环引用?
循环引用,顾名思义,是指两个或多个对象通过 shared_ptr 相互持有对方的强引用,形成一个闭环。在这种情况下,即使外部已经没有任何 shared_ptr 指向这个环路中的任何对象,环路中的每个对象的强引用计数也永远不会降到零。结果就是,这些对象永远不会被销毁,它们的内存会一直被占用,直到程序终止。
形式化描述:
假设对象 A 持有指向对象 B 的 shared_ptr,同时对象 B 也持有指向对象 A 的 shared_ptr。
- A 的强引用计数 ≥ 1 (因为它被 B 引用着)
- B 的强引用计数 ≥ 1 (因为它被 A 引用着)
即使所有指向 A 和 B 的外部 shared_ptr 都被销毁,A 和 B 的内部强引用计数仍然保持为 1。它们会互相阻止对方的销毁,导致内存泄漏。
经典场景分析
循环引用并非罕见,它经常出现在需要对象之间建立双向关联的设计中。
场景一:双向链表 (Doubly Linked List)
双向链表是学习循环引用的最经典案例。每个节点都包含指向下一个节点和前一个节点的指针。如果这两个指针都使用 shared_ptr,就很容易形成循环。
问题代码示例:
#include <iostream>
#include <memory>
#include <string>
// 前向声明 Node 类,因为 Node 内部会使用 shared_ptr<Node>
class Node;
class Node {
public:
std::string data;
std::shared_ptr<Node> next; // 指向下一个节点的强引用
std::shared_ptr<Node> prev; // 指向前一个节点的强引用
Node(std::string d) : data(std::move(d)) {
std::cout << "Node " << data << " constructed." << std::endl;
}
~Node() {
std::cout << "Node " << data << " destructed." << std::endl;
}
void print() const {
std::cout << "Node data: " << data;
if (next) {
std::cout << ", next: " << next->data;
}
if (prev) {
std::cout << ", prev: " << prev->data;
}
std::cout << std::endl;
}
};
void create_and_leak_list() {
std::shared_ptr<Node> head = std::make_shared<Node>("Head");
std::shared_ptr<Node> middle = std::make_shared<Node>("Middle");
std::shared_ptr<Node> tail = std::make_shared<Node>("Tail");
// 建立链表关系
head->next = middle;
middle->prev = head;
middle->next = tail;
tail->prev = middle;
std::cout << "n--- Initial state ---" << std::endl;
std::cout << "Head use_count: " << head.use_count() << std::endl; // 1 (被 middle->prev 引用) + 1 (head 本身) = 2
std::cout << "Middle use_count: " << middle.use_count() << std::endl; // 1 (被 head->next 引用) + 1 (被 tail->prev 引用) + 1 (middle 本身) = 3
std::cout << "Tail use_count: " << tail.use_count() << std::endl; // 1 (被 middle->next 引用) + 1 (tail 本身) = 2
// 尝试断开外部引用
head.reset();
middle.reset();
tail.reset();
std::cout << "n--- After resetting external shared_ptrs ---" << std::endl;
// 观察析构函数是否被调用
// 这里我们将无法看到 Node 对象的析构信息
}
int main() {
std::cout << "Starting create_and_leak_list()..." << std::endl;
create_and_leak_list();
std::cout << "Finished create_and_leak_list()." << std::endl;
std::cout << "Program exiting. If Nodes were not destructed, it's a leak." << std::endl;
return 0;
}
分析:
head被middle->prev强引用。middle被head->next和tail->prev强引用。tail被middle->next强引用。
当 head, middle, tail 这三个局部 shared_ptr 离开 create_and_leak_list 作用域时,它们的强引用计数会减一。
head的use_count从 2 变为 1 (因为它还被middle->prev引用)。middle的use_count从 3 变为 2 (因为它还被head->next和tail->prev引用)。tail的use_count从 2 变为 1 (因为它还被middle->next引用)。
由于任何一个节点的 use_count 都未能降到 0,因此没有任何节点会被销毁,它们所占用的内存会一直泄漏。
场景二:父子关系 (Parent-Child Relationships)
在许多面向对象设计中,我们会建立父子关系。如果父对象持有子对象的 shared_ptr,同时子对象又持有父对象的 shared_ptr,也可能导致循环引用。
问题代码示例:
#include <iostream>
#include <memory>
#include <vector>
#include <string>
class Child; // 前向声明
class Parent {
public:
std::string name;
std::vector<std::shared_ptr<Child>> children; // 父拥有子
Parent(std::string n) : name(std::move(n)) {
std::cout << "Parent " << name << " constructed." << std::endl;
}
~Parent() {
std::cout << "Parent " << name << " destructed." << std::endl;
}
void addChild(std::shared_ptr<Child> child);
};
class Child {
public:
std::string name;
std::shared_ptr<Parent> parent; // 子拥有父
Child(std::string n) : name(std::move(n)) {
std::cout << "Child " << name << " constructed." << std::endl;
}
~Child() {
std::cout << "Child " << name << " destructed." << std::endl;
}
void setParent(std::shared_ptr<Parent> p) {
parent = std::move(p);
}
};
void Parent::addChild(std::shared_ptr<Child> child) {
children.push_back(child);
// 建立子到父的引用
child->setParent(std::shared_ptr<Parent>(this)); // 错误:这里创建一个新的独立的 shared_ptr
// 无法正确增加外部 shared_ptr 的引用计数
// 正确做法是使用 enable_shared_from_this, 但此处是为了演示循环引用
}
void create_parent_child_leak() {
std::shared_ptr<Parent> p = std::make_shared<Parent>("Alice");
std::shared_ptr<Child> c1 = std::make_shared<Child>("Bob");
std::shared_ptr<Child> c2 = std::make_shared<Child>("Charlie");
// 这里为了演示循环引用,我们手动建立循环。
// 在实际 `Parent::addChild` 内部,需要使用 `enable_shared_from_this` 来获取指向自身的 shared_ptr
// 但即使使用了 `enable_shared_from_this`,如果子对象反向持有父对象的 `shared_ptr`,依然会形成循环。
// 父持有子
p->children.push_back(c1);
p->children.push_back(c2);
// 子持有父 (手动建立循环)
c1->parent = p;
c2->parent = p;
std::cout << "n--- Initial state ---" << std::endl;
std::cout << "Parent '" << p->name << "' use_count: " << p.use_count() << std::endl; // 1 (p本身) + 1 (c1->parent) + 1 (c2->parent) = 3
std::cout << "Child '" << c1->name << "' use_count: " << c1.use_count() << std::endl; // 1 (c1本身) + 1 (p->children[0]) = 2
std::cout << "Child '" << c2->name << "' use_count: " << c2.use_count() << std::endl; // 1 (c2本身) + 1 (p->children[1]) = 2
// 外部 shared_ptr 离开作用域
} // p, c1, c2 离开作用域,引用计数减1
int main() {
std::cout << "Starting create_parent_child_leak()..." << std::endl;
create_parent_child_leak();
std::cout << "Finished create_parent_child_leak()." << std::endl;
std::cout << "Program exiting. If Parent/Child were not destructed, it's a leak." << std::endl;
return 0;
}
分析:
Parent对象被p、c1->parent和c2->parent强引用。Child对象c1被c1和p->children[0]强引用。Child对象c2被c2和p->children[1]强引用。
当 p, c1, c2 离开 create_parent_child_leak 作用域时,p 的 use_count 减 1 (从 3 变为 2),c1 和 c2 的 use_count 减 1 (从 2 变为 1)。
所有对象的强引用计数都未能降到 0,形成循环:p 引用 c1 和 c2,而 c1 和 c2 又引用 p。它们互相阻止对方的销毁,导致内存泄漏。
场景三:观察者模式/发布订阅模式 (Observer/Publish-Subscribe Pattern)
在观察者模式中,通常有一个主题 (Subject) 维护一个观察者 (Observer) 列表。当事件发生时,主题会通知所有注册的观察者。如果主题持有观察者的 shared_ptr,同时观察者又持有主题的 shared_ptr (例如,为了在某个时刻取消订阅),那么也可能产生循环引用。
问题代码示例:
#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <algorithm> // for std::remove_if
class Subject; // 前向声明
class Observer {
public:
std::string name;
std::shared_ptr<Subject> observed_subject; // 观察者持有主题的强引用
Observer(std::string n) : name(std::move(n)) {
std::cout << "Observer " << name << " constructed." << std::endl;
}
~Observer() {
std::cout << "Observer " << name << " destructed." << std::endl;
}
void update(const std::string& message) {
std::cout << "Observer " << name << " received message: " << message << std::endl;
}
// 假设 Observer 需要自行取消订阅,所以它需要持有 Subject 的 shared_ptr
void unsubscribe(); // 定义在 Subject 之后
};
class Subject {
public:
std::string name;
std::vector<std::shared_ptr<Observer>> observers; // 主题持有观察者的强引用
Subject(std::string n) : name(std::move(n)) {
std::cout << "Subject " << name << " constructed." << std::endl;
}
~Subject() {
std::cout << "Subject " << name << " destructed." << std::endl;
}
void attach(std::shared_ptr<Observer> obs) {
observers.push_back(obs);
}
void detach(std::shared_ptr<Observer> obs) {
// 通常需要比较 obs 的原始指针,这里简化处理
observers.erase(std::remove_if(observers.begin(), observers.end(),
[&](const std::shared_ptr<Observer>& p) { return p == obs; }),
observers.end());
}
void notify(const std::string& message) {
std::cout << "Subject " << name << " notifying observers..." << std::endl;
for (const auto& obs_ptr : observers) {
if (obs_ptr) {
obs_ptr->update(message);
}
}
}
};
void Observer::unsubscribe() {
if (observed_subject) {
std::cout << "Observer " << name << " unsubscribing from " << observed_subject->name << std::endl;
observed_subject->detach(std::shared_ptr<Observer>(this)); // 错误:这里创建了一个新的 shared_ptr
// 正确做法是使用 enable_shared_from_this
}
}
void create_observer_leak() {
std::shared_ptr<Subject> news_feed = std::make_shared<Subject>("Daily News");
std::shared_ptr<Observer> reader1 = std::make_shared<Observer>("John");
std::shared_ptr<Observer> reader2 = std::make_shared<Observer>("Jane");
// 主题注册观察者
news_feed->attach(reader1);
news_feed->attach(reader2);
// 观察者持有主题 (手动建立循环)
reader1->observed_subject = news_feed;
reader2->observed_subject = news_feed;
std::cout << "n--- Initial state ---" << std::endl;
std::cout << "Subject '" << news_feed->name << "' use_count: " << news_feed.use_count() << std::endl; // 1 (news_feed本身) + 1 (reader1->observed_subject) + 1 (reader2->observed_subject) = 3
std::cout << "Observer '" << reader1->name << "' use_count: " << reader1.use_count() << std::endl; // 1 (reader1本身) + 1 (news_feed->observers[0]) = 2
std::cout << "Observer '" << reader2->name << "' use_count: " << reader2.use_count() << std::endl; // 1 (reader2本身) + 1 (news_feed->observers[1]) = 2
// 外部 shared_ptr 离开作用域
} // news_feed, reader1, reader2 离开作用域
int main() {
std::cout << "Starting create_observer_leak()..." << std::endl;
create_observer_leak();
std::cout << "Finished create_observer_leak()." << std::endl;
std::cout << "Program exiting. If Subject/Observers were not destructed, it's a leak." << std::endl;
return 0;
}
分析:
Subject对象被news_feed、reader1->observed_subject和reader2->observed_subject强引用。Observer对象reader1被reader1和news_feed->observers[0]强引用。Observer对象reader2被reader2和news_feed->observers[1]强引用。
当 news_feed, reader1, reader2 离开 create_observer_leak 作用域时,它们的强引用计数减 1。
news_feed的use_count从 3 变为 2。reader1的use_count从 2 变为 1。reader2的use_count从 2 变为 1。
同样,没有对象的强引用计数降为 0,导致循环引用和内存泄漏。
这些案例清晰地展示了循环引用是如何在看似合理的双向关联中悄然形成的。要解决这个问题,我们需要引入一个不计入强引用计数的“弱”指针。
weak_ptr:破局利器
为了打破 shared_ptr 引起的循环引用,C++11 引入了 std::weak_ptr。weak_ptr 是 shared_ptr 的伴侣,它的设计目的就是为了解决循环引用问题。
weak_ptr 的设计哲学
weak_ptr 的核心思想是:它不拥有对象,只观测对象。
- 不拥有对象:
weak_ptr不增加所指向对象的强引用计数。这意味着weak_ptr的存在不会阻止对象被销毁。 - 只观测对象:
weak_ptr提供了一种方式来安全地访问它所观测的对象。你可以查询对象是否仍然存活,并在对象存活时获取一个临时的shared_ptr来安全地使用它。
weak_ptr 的基本用法
-
创建
weak_ptr:weak_ptr可以从shared_ptr或另一个weak_ptr构造。std::shared_ptr<MyObject> s_ptr = std::make_shared<MyObject>(); std::weak_ptr<MyObject> w_ptr = s_ptr; // 从 shared_ptr 创建 weak_ptr此时
s_ptr的强引用计数为 1,w_ptr不会增加强引用计数。但w_ptr会增加控制块的弱引用计数。 -
lock()方法: 这是weak_ptr最重要的方法。它尝试获取一个shared_ptr。- 如果
weak_ptr观测的对象仍然存活 (即其强引用计数大于 0),lock()会返回一个有效的shared_ptr,并增加对象的强引用计数。 - 如果观测的对象已经销毁,
lock()会返回一个空的shared_ptr。std::shared_ptr<MyObject> s_ptr = std::make_shared<MyObject>(); std::weak_ptr<MyObject> w_ptr = s_ptr;
if (auto locked_ptr = w_ptr.lock()) { // 尝试获取 shared_ptr
// 对象仍然存活,可以安全使用 locked_ptr
locked_ptr->greet();
std::cout << "s_ptr use_count: " << s_ptr.use_count() << std::endl; // 2 (因为 locked_ptr 增加了计数)
} else {
std::cout << "Object already expired." << std::endl;
}s_ptr.reset(); // 销毁 s_ptr,对象强引用计数归零,MyObject 被销毁
if (auto locked_ptr = w_ptr.lock()) {
// 不会进入这里
} else {
std::cout << "Object expired after s_ptr reset." << std::endl; // 输出此行
} - 如果
-
expired()方法: 检查weak_ptr观测的对象是否已经销毁。如果已销毁,返回true。if (w_ptr.expired()) { std::cout << "Object expired." << std::endl; }通常,
lock()方法更推荐,因为它既检查了对象的存活状态,又在对象存活时提供了安全的shared_ptr。 -
use_count()方法: 返回所观测对象的强引用计数。与shared_ptr::use_count()相同。std::shared_ptr<MyObject> s_ptr = std::make_shared<MyObject>(); std::weak_ptr<MyObject> w_ptr = s_ptr; std::cout << "Weak ptr's observed strong use_count: " << w_ptr.use_count() << std::endl; // 1
weak_ptr 如何打破循环引用
解决循环引用的关键在于:将环路中的一个强引用替换为弱引用。 这样,这个弱引用就不会阻止对象被销毁。
让我们用 weak_ptr 来重构之前的泄漏示例。
解决方案一:双向链表 (Doubly Linked List)
在双向链表中,next 指针通常是“拥有”下一个节点的,而 prev 指针只是“观测”前一个节点。因此,将 prev 指针改为 weak_ptr 即可。
#include <iostream>
#include <memory>
#include <string>
class Node;
class Node {
public:
std::string data;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 使用 weak_ptr 解决循环引用
Node(std::string d) : data(std::move(d)) {
std::cout << "Node " << data << " constructed." << std::endl;
}
~Node() {
std::cout << "Node " << data << " destructed." << std::endl;
}
void print() const {
std::cout << "Node data: " << data;
if (next) {
std::cout << ", next: " << next->data;
}
// 对于 weak_ptr,需要 lock() 才能安全访问
if (auto p = prev.lock()) {
std::cout << ", prev: " << p->data;
} else {
std::cout << ", prev: expired";
}
std::cout << std::endl;
}
};
void create_and_fix_list() {
std::shared_ptr<Node> head = std::make_shared<Node>("Head");
std::shared_ptr<Node> middle = std::make_shared<Node>("Middle");
std::shared_ptr<Node> tail = std::make_shared<Node>("Tail");
// 建立链表关系
head->next = middle;
middle->prev = head; // middle->prev 是 weak_ptr,不增加 head 的强引用计数
middle->next = tail;
tail->prev = middle; // tail->prev 是 weak_ptr,不增加 middle 的强引用计数
std::cout << "n--- Initial state ---" << std::endl;
std::cout << "Head use_count: " << head.use_count() << std::endl; // 1 (head 本身)
std::cout << "Middle use_count: " << middle.use_count() << std::endl; // 1 (middle 本身) + 1 (被 head->next 引用) = 2
std::cout << "Tail use_count: " << tail.use_count() << std::endl; // 1 (tail 本身) + 1 (被 middle->next 引用) = 2
// 尝试断开外部引用
head.reset();
middle.reset();
tail.reset();
std::cout << "n--- After resetting external shared_ptrs ---" << std::endl;
// 观察析构函数是否被调用
// 这里我们将看到所有 Node 对象的析构信息,证明泄漏被修复
}
int main() {
std::cout << "Starting create_and_fix_list()..." << std::endl;
create_and_fix_list();
std::cout << "Finished create_and_fix_list()." << std::endl;
std::cout << "Program exiting. All Nodes should be destructed." << std::endl;
return 0;
}
分析:
当 head, middle, tail 这三个局部 shared_ptr 离开作用域时:
tail.reset():tail局部shared_ptr消失。middle->next仍然强引用tail。tail的强引用计数为 1。middle.reset():middle局部shared_ptr消失。head->next仍然强引用middle。tail->prev是weak_ptr,不影响middle的强引用计数。middle的强引用计数为 1。head.reset():head局部shared_ptr消失。middle->prev是weak_ptr,不影响head的强引用计数。head的强引用计数为 0,Head对象被销毁。
当 Head 对象销毁时,head->next (即指向 middle 的 shared_ptr) 也被销毁。这导致 middle 的强引用计数从 1 变为 0,Middle 对象被销毁。
当 Middle 对象销毁时,middle->next (即指向 tail 的 shared_ptr) 也被销毁。这导致 tail 的强引用计数从 1 变为 0,Tail 对象被销毁。
所有对象都被正确销毁,内存泄漏问题得到解决。
解决方案二:父子关系 (Parent-Child Relationships)
在父子关系中,通常的语义是父拥有子,子知道父但并不拥有父。因此,子指向父的引用应该使用 weak_ptr。
#include <iostream>
#include <memory>
#include <vector>
#include <string>
class Child;
class Parent {
public:
std::string name;
std::vector<std::shared_ptr<Child>> children; // 父拥有子
Parent(std::string n) : name(std::move(n)) {
std::cout << "Parent " << name << " constructed." << std::endl;
}
~Parent() {
std::cout << "Parent " << name << " destructed." << std::endl;
}
void addChild(std::shared_ptr<Child> child);
};
class Child {
public:
std::string name;
std::weak_ptr<Parent> parent; // 子观测父,使用 weak_ptr
Child(std::string n) : name(std::move(n)) {
std::cout << "Child " << name << " constructed." << std::endl;
}
~Child() {
std::cout << "Child " << name << " destructed." << std::endl;
}
void setParent(std::weak_ptr<Parent> p) { // 接受 weak_ptr
parent = std::move(p);
}
};
void create_parent_child_fixed() {
std::shared_ptr<Parent> p = std::make_shared<Parent>("Alice");
std::shared_ptr<Child> c1 = std::make_shared<Child>("Bob");
std::shared_ptr<Child> c2 = std::make_shared<Child>("Charlie");
// 父持有子
p->children.push_back(c1);
p->children.push_back(c2);
// 子观测父 (通过 shared_ptr 转换为 weak_ptr)
c1->setParent(p); // 从 shared_ptr<Parent> p 隐式转换为 weak_ptr<Parent>
c2->setParent(p);
std::cout << "n--- Initial state ---" << std::endl;
// p 的 use_count: 1 (p本身) + 0 (c1->parent是weak_ptr) + 0 (c2->parent是weak_ptr) = 1
std::cout << "Parent '" << p->name << "' use_count: " << p.use_count() << std::endl;
// c1 的 use_count: 1 (c1本身) + 1 (p->children[0]) = 2
std::cout << "Child '" << c1->name << "' use_count: " << c1.use_count() << std::endl;
// c2 的 use_count: 1 (c2本身) + 1 (p->children[1]) = 2
std::cout << "Child '" << c2->name << "' use_count: " << c2.use_count() << std::endl;
// 外部 shared_ptr 离开作用域
} // p, c1, c2 离开作用域
int main() {
std::cout << "Starting create_parent_child_fixed()..." << std::endl;
create_parent_child_fixed();
std::cout << "Finished create_parent_child_fixed()." << std::endl;
std::cout << "Program exiting. All Parent/Child should be destructed." << std::endl;
return 0;
}
分析:
当 p, c1, c2 离开 create_parent_child_fixed 作用域时:
p局部shared_ptr消失。p的强引用计数从 1 变为 0。Parent对象被销毁。- 当
Parent对象p销毁时,其children向量中的shared_ptr(指向c1和c2) 也被销毁。 - 这导致
c1和c2的强引用计数从 2 变为 1 (只剩下局部shared_ptr c1和c2自身)。 - 接着,
c1和c2局部shared_ptr消失。它们的强引用计数从 1 变为 0。Child对象c1和c2被销毁。
所有对象都被正确销毁。
解决方案三:观察者模式 (Observer/Publish-Subscribe Pattern)
在观察者模式中,主题通常不应该“拥有”观察者,因为观察者可能在任何时候被销毁。主题只需要知道它们的存在。因此,主题持有观察者的 weak_ptr 是更合理的设计。
#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <algorithm> // for std::remove_if
class Subject; // 前向声明
class Observer {
public:
std::string name;
// 观察者通常不需要持有主题的 shared_ptr。如果需要取消订阅,可以通过参数传入主题的 shared_ptr
// 或者,如果非要持有,也应是 weak_ptr。这里为了演示,假设 Observer 拥有 Subject 的 weak_ptr。
std::weak_ptr<Subject> observed_subject;
Observer(std::string n) : name(std::move(n)) {
std::cout << "Observer " << name << " constructed." << std::endl;
}
~Observer() {
std::cout << "Observer " << name << " destructed." << std::endl;
}
void update(const std::string& message) {
std::cout << "Observer " << name << " received message: " << message << std::endl;
}
void setSubject(std::weak_ptr<Subject> s) {
observed_subject = s;
}
void unsubscribe(); // 定义在 Subject 之后
};
class Subject {
public:
std::string name;
// 主题不拥有观察者,只观测它们。使用 weak_ptr。
std::vector<std::weak_ptr<Observer>> observers;
Subject(std::string n) : name(std::move(n)) {
std::cout << "Subject " << name << " constructed." << std::endl;
}
~Subject() {
std::cout << "Subject " << name << " destructed." << std::endl;
}
void attach(std::shared_ptr<Observer> obs) {
observers.push_back(obs); // 从 shared_ptr 隐式转换为 weak_ptr
}
void detach(std::shared_ptr<Observer> obs) {
// 移除已失效或匹配的观察者
observers.erase(std::remove_if(observers.begin(), observers.end(),
[&](const std::weak_ptr<Observer>& wp) {
if (auto sp = wp.lock()) {
return sp == obs; // 比较 shared_ptr
}
return true; // 如果 weak_ptr 已经过期,也移除
}),
observers.end());
}
void notify(const std::string& message) {
std::cout << "Subject " << name << " notifying observers..." << std::endl;
// 在通知前清理已失效的观察者
observers.erase(std::remove_if(observers.begin(), observers.end(),
[](const std::weak_ptr<Observer>& wp) {
return wp.expired(); // 移除已失效的 weak_ptr
}),
observers.end());
for (const auto& obs_wp : observers) {
if (auto obs_sp = obs_wp.lock()) { // 尝试获取 shared_ptr
obs_sp->update(message);
}
}
}
};
void Observer::unsubscribe() {
if (auto s_ptr = observed_subject.lock()) { // 尝试获取主题的 shared_ptr
std::cout << "Observer " << name << " unsubscribing from " << s_ptr->name << std::endl;
// 这里需要一个指向自身的 shared_ptr,需要用到 enable_shared_from_this
// 假设这里能获得有效的 shared_ptr<Observer>(this)
// 为了演示,这里简化为 s_ptr->detach(nullptr); 实际需要传递 this 对应的 shared_ptr
// s_ptr->detach(std::shared_ptr<Observer>(this)); // 这仍然是错误的用法
// 更好的做法是让 Subject 知道如何移除一个 Observer,或者 Observer 接收一个 shared_ptr<Observer> 自身的引用
}
}
void create_observer_fixed() {
std::shared_ptr<Subject> news_feed = std::make_shared<Subject>("Daily News");
std::shared_ptr<Observer> reader1 = std::make_shared<Observer>("John");
std::shared_ptr<Observer> reader2 = std::make_shared<Observer>("Jane");
// 主题注册观察者 (weak_ptr)
news_feed->attach(reader1);
news_feed->attach(reader2);
// 观察者观测主题 (weak_ptr)
reader1->setSubject(news_feed);
reader2->setSubject(news_feed);
std::cout << "n--- Initial state ---" << std::endl;
// news_feed 的 use_count: 1 (news_feed本身) + 0 (reader1->observed_subject是weak_ptr) + 0 (reader2->observed_subject是weak_ptr) = 1
std::cout << "Subject '" << news_feed->name << "' use_count: " << news_feed.use_count() << std::endl;
// reader1 的 use_count: 1 (reader1本身) + 0 (news_feed->observers[0]是weak_ptr) = 1
std::cout << "Observer '" << reader1->name << "' use_count: " << reader1.use_count() << std::endl;
// reader2 的 use_count: 1 (reader2本身) + 0 (news_feed->observers[1]是weak_ptr) = 1
std::cout << "Observer '" << reader2->name << "' use_count: " << reader2.use_count() << std::endl;
// 外部 shared_ptr 离开作用域
} // news_feed, reader1, reader2 离开作用域
int main() {
std::cout << "Starting create_observer_fixed()..." << std::endl;
create_observer_fixed();
std::cout << "Finished create_observer_fixed()." << std::endl;
std::cout << "Program exiting. All Subject/Observers should be destructed." << std::endl;
return 0;
}
分析:
当 news_feed, reader1, reader2 离开 create_observer_fixed 作用域时:
news_feed局部shared_ptr消失。news_feed的强引用计数从 1 变为 0。Subject对象被销毁。reader1局部shared_ptr消失。reader1的强引用计数从 1 变为 0。Observer对象reader1被销毁。reader2局部shared_ptr消失。reader2的强引用计数从 1 变为 0。Observer对象reader2被销毁。
所有对象都被正确销毁,再次证明 weak_ptr 的有效性。
weak_ptr 的适用场景与最佳实践
weak_ptr 不仅仅是解决循环引用的工具,它在许多其他场景中也发挥着重要作用。
打破循环引用
这是 weak_ptr 最主要也是最直接的应用。
- 父子关系: 父对象拥有子对象 (使用
shared_ptr),子对象观测父对象 (使用weak_ptr)。 - 双向关联: 在双向链表或其他互相引用的数据结构中,选择一方使用
shared_ptr(拥有),另一方使用weak_ptr(观测)。通常,拥有生命周期长或更重要的对象的那一方使用shared_ptr,反向引用使用weak_ptr。 - 对象图: 在复杂的对象图中,仔细分析对象的所有权语义,确保没有强引用循环。
缓存管理 (Cache Management)
weak_ptr 非常适合用于实现缓存。缓存中的对象应该在不再被外部强引用时被回收,以节省内存。
#include <iostream>
#include <memory>
#include <map>
#include <string>
class Resource {
public:
std::string id;
Resource(std::string id_val) : id(std::move(id_val)) {
std::cout << "Resource " << id << " constructed." << std::endl;
}
~Resource() {
std::cout << "Resource " << id << " destructed." << std::endl;
}
void use() {
std::cout << "Using resource " << id << std::endl;
}
};
class ResourceManager {
public:
std::shared_ptr<Resource> getResource(const std::string& id) {
// 尝试从缓存获取
if (cache_.count(id)) {
if (auto sp = cache_[id].lock()) { // 尝试锁定 weak_ptr
std::cout << "Cache hit for " << id << std::endl;
return sp;
} else {
// 资源已过期,从缓存中移除
std::cout << "Cache entry for " << id << " expired." << std::endl;
cache_.erase(id);
}
}
// 缓存未命中或已过期,创建新资源并放入缓存
std::cout << "Creating new resource " << id << std::endl;
std::shared_ptr<Resource> new_res = std::make_shared<Resource>(id);
cache_[id] = new_res; // 将 shared_ptr 存为 weak_ptr
return new_res;
}
private:
std::map<std::string, std::weak_ptr<Resource>> cache_;
};
void test_cache() {
ResourceManager rm;
std::shared_ptr<Resource> r1 = rm.getResource("A"); // 创建 A
r1->use();
std::shared_ptr<Resource> r2 = rm.getResource("A"); // 缓存命中 A
r2->use();
std::shared_ptr<Resource> r3 = rm.getResource("B"); // 创建 B
r3->use();
std::cout << "n--- Releasing r1 and r2 ---n";
r1.reset(); // A 的强引用计数变为 1 (被 r2 引用)
r2.reset(); // A 的强引用计数变为 0,A 被销毁
std::shared_ptr<Resource> r4 = rm.getResource("A"); // A 已销毁,重新创建 A
r4->use();
std::cout << "n--- Releasing r3 and r4 ---n";
r3.reset(); // B 的强引用计数变为 0,B 被销毁
r4.reset(); // A 的强引用计数变为 0,A 被销毁
}
int main() {
test_cache();
return 0;
}
在这个例子中,即使 ResourceManager 缓存了 Resource 对象的 weak_ptr,只要没有外部 shared_ptr 引用该 Resource,它就会被销毁,从而释放内存。当再次请求该 Resource 时,如果它已销毁,ResourceManager 会重新创建。
观察者模式 (Observer Pattern)
如前所述,主题 (Subject) 维护观察者 (Observer) 列表时,如果主题持有观察者的 weak_ptr,可以避免循环引用,并且当观察者对象自身被销毁时,主题不会阻止其销毁。
// 参见前面 "解决方案三:观察者模式" 中的代码示例。
// 主要思想是 Subject 内部的 observers 列表存储 weak_ptr<Observer>。
避免悬空指针 (Avoiding Dangling Pointers)
weak_ptr 本身并不能直接访问对象,必须通过 lock() 方法转换为 shared_ptr。如果对象已经销毁,lock() 会返回一个空的 shared_ptr,这使得程序能够安全地判断对象是否存活,从而避免了使用悬空指针的风险。这是 weak_ptr 比原始指针更安全的地方。
性能考量
- 创建和销毁:
weak_ptr的创建和销毁需要访问和修改控制块中的弱引用计数,这会带来一定的开销,通常略高于shared_ptr。 lock()操作:lock()操作也需要访问控制块,并可能涉及到原子操作来增加强引用计数,这也有少量开销。- 内存占用:
weak_ptr通常与shared_ptr占用相同的内存大小 (通常是两个指针大小,一个指向对象,一个指向控制块)。
在大多数应用中,weak_ptr 的性能开销可以忽略不计。只有在极端性能敏感、高并发的场景下,才需要仔细评估其影响。
设计模式中的应用
weak_ptr 在许多设计模式中都有用武之地,尤其是在需要建立对象间非所有权关系时:
- 工厂模式: 如果工厂需要跟踪它创建的对象,但又不希望阻止这些对象被销毁,可以使用
weak_ptr。 - 享元模式 (Flyweight Pattern): 共享对象池可能使用
weak_ptr来管理共享对象的生命周期,以便在不再需要时回收。
智能指针家族的协作
C++11 引入了三种主要的智能指针:unique_ptr、shared_ptr 和 weak_ptr。它们各自有不同的所有权语义和适用场景,通常需要协作才能发挥最大效用。
unique_ptr 简介
unique_ptr 实现了独占所有权语义。
- 一个
unique_ptr实例独占一个对象。 - 不能拷贝,只能通过移动语义转移所有权。
- 当
unique_ptr离开作用域时,它所管理的对象会被销毁。 - 它比
shared_ptr更轻量级,没有引用计数开销,性能更好。 - 适用于对象生命周期明确、且只有一个所有者的情况。
#include <memory>
#include <iostream>
class SimpleObject {
public:
SimpleObject() { std::cout << "SimpleObject constructed." << std::endl; }
~SimpleObject() { std::cout << "SimpleObject destructed." << std::endl; }
void foo() { std::cout << "SimpleObject::foo()" << std::endl; }
};
void test_unique_ptr() {
std::unique_ptr<SimpleObject> u1 = std::make_unique<SimpleObject>(); // C++14 make_unique
u1->foo();
// std::unique_ptr<SimpleObject> u2 = u1; // 编译错误:unique_ptr 不能拷贝
std::unique_ptr<SimpleObject> u3 = std::move(u1); // 移动所有权
u3->foo();
if (!u1) { // u1 现在为空
std::cout << "u1 is now null." << std::endl;
}
} // u3 离开作用域,SimpleObject 被销毁
int main() {
test_unique_ptr();
return 0;
}
何时选择哪种智能指针
| 智能指针 | 所有权语义 | 拷贝/移动 | 引用计数 | 性能开销 | 典型场景 |
|---|---|---|---|---|---|
unique_ptr |
独占 | 只能移动 | 无 | 最低 | 单一所有者、RAII 管理、工厂函数返回 |
shared_ptr |
共享 | 可拷贝 | 有 | 较高 | 共享所有权、对象生命周期复杂、对象图 |
weak_ptr |
观测 | 可拷贝 | 无 | 较低 | 打破循环引用、缓存、观察者模式、安全访问可能已失效的对象 |
选择指导原则:
- 优先使用
unique_ptr: 如果对象只有一个明确的所有者,且其生命周期与所有者绑定,那么unique_ptr是最佳选择。它提供了最轻量级的内存管理。 - 需要共享所有权时使用
shared_ptr: 当多个部分需要共同管理一个对象的生命周期,并且没有一个明确的“唯一所有者”时,shared_ptr是合适的。 - 打破循环引用或观测对象时使用
weak_ptr: 当你在使用shared_ptr时遇到循环引用问题,或者需要观测一个对象但又不希望影响其生命周期时,weak_ptr是解决方案。
智能指针间的转换
unique_ptr到shared_ptr: 可以通过移动构造将unique_ptr转换为shared_ptr。这表示所有权从独占变为共享。std::unique_ptr<MyObject> u_ptr = std::make_unique<MyObject>(); std::shared_ptr<MyObject> s_ptr = std::move(u_ptr); // 所有权转移 // u_ptr 此时为空shared_ptr到weak_ptr: 可以直接赋值构造,weak_ptr观测shared_ptr所指向的对象。std::shared_ptr<MyObject> s_ptr = std::make_shared<MyObject>(); std::weak_ptr<MyObject> w_ptr = s_ptr;weak_ptr到shared_ptr: 必须通过lock()方法。std::weak_ptr<MyObject> w_ptr = s_ptr; if (auto locked_s_ptr = w_ptr.lock()) { // 使用 locked_s_ptr }shared_ptr到unique_ptr: 不支持直接转换。因为shared_ptr可能有多个所有者,不能简单地“剥夺”所有权给unique_ptr。如果确实需要,你可能需要重新设计所有权关系,或者通过shared_ptr::get()获取原始指针并手动创建unique_ptr(但这通常不安全,因为shared_ptr可能在unique_ptr销毁前释放对象)。
代码示例:智能指针协作
#include <iostream>
#include <memory>
#include <vector>
class Gadget {
public:
std::string name;
Gadget(std::string n) : name(std::move(n)) {
std::cout << "Gadget " << name << " constructed." << std::endl;
}
~Gadget() {
std::cout << "Gadget " << name << " destructed." << std::endl;
}
};
class Robot {
public:
std::string name;
std::unique_ptr<Gadget> primary_gadget; // 独占主要配件
std::vector<std::shared_ptr<Gadget>> shared_gadgets; // 共享一些配件
Robot(std::string n, std::unique_ptr<Gadget> pg)
: name(std::move(n)), primary_gadget(std::move(pg)) {
std::cout << "Robot " << name << " constructed." << std::endl;
}
~Robot() {
std::cout << "Robot " << name << " destructed." << std::endl;
}
void addSharedGadget(std::shared_ptr<Gadget> sg) {
shared_gadgets.push_back(sg);
}
};
void test_smart_pointer_collaboration() {
// 创建一个独占的 Gadget
auto laser = std::make_unique<Gadget>("Laser");
// 创建机器人,并将独占 Gadget 移动给它
auto r1 = std::make_shared<Robot>("RoboCop", std::move(laser));
// laser 此时为空
// 创建一个共享的 Gadget
auto power_cell = std::make_shared<Gadget>("Power Cell");
// 机器人共享 power_cell
r1->addSharedGadget(power_cell);
// 另一个机器人也共享 power_cell
auto r2_primary_gadget = std::make_unique<Gadget>("Drill");
auto r2 = std::make_shared<Robot>("Terminator", std::move(r2_primary_gadget));
r2->addSharedGadget(power_cell);
std::cout << "n--- Initial state ---n";
std::cout << "Power Cell use_count: " << power_cell.use_count() << std::endl; // 1 (power_cell本身) + 1 (r1->shared_gadgets[0]) + 1 (r2->shared_gadgets[0]) = 3
// 假设 r1 离开了作用域或者被 reset
std::cout << "n--- Resetting r1 ---n";
r1.reset(); // RoboCop 和 Laser 析构
std::cout << "Power Cell use_count: " << power_cell.use_count() << std::endl; // 2
std::cout << "n--- Resetting r2 ---n";
r2.reset(); // Terminator 和 Drill 析构
std::cout << "Power Cell use_count: " << power_cell.use_count() << std::endl; // 1
std::cout << "n--- Releasing power_cell ---n";
} // power_cell 离开作用域,Power Cell 析构
int main() {
test_smart_pointer_collaboration();
return 0;
}
这个例子展示了 unique_ptr 和 shared_ptr 如何根据所有权语义进行协作,共同管理不同的资源。
高级议题与注意事项
enable_shared_from_this 机制
当一个类希望其成员函数能够获取一个指向其自身对象的 shared_ptr 时,直接使用 std::shared_ptr<T>(this) 是一个非常危险的错误。这会创建一个独立的 shared_ptr 控制块,与外部已有的 shared_ptr 管理的对象是同一个,但引用计数独立。最终会导致对象被多次析构,造成未定义行为。
为了解决这个问题,标准库提供了 std::enable_shared_from_this<T>。
任何想要在成员函数中安全地获取自身 shared_ptr 的类,都应该公开继承 std::enable_shared_from_this<T>。然后,在成员函数中通过 shared_from_this() 方法获取 shared_ptr。
原理: enable_shared_from_this 内部维护了一个 weak_ptr<T> 指向自身。当第一个 shared_ptr 被创建并指向这个对象时,这个内部的 weak_ptr 就会被初始化。之后,shared_from_this() 只是调用这个内部 weak_ptr 的 lock() 方法,从而安全地获取到指向自身的 shared_ptr。
#include <iostream>
#include <memory>
#include <vector>
class SelfReferencing : public std::enable_shared_from_this<SelfReferencing> {
public:
std::string name;
SelfReferencing(std::string n) : name(std::move(n)) {
std::cout << "SelfReferencing " << name << " constructed." << std::endl;
}
~SelfReferencing() {
std::cout << "SelfReferencing " << name << " destructed." << std::endl;
}
// 正确获取自身 shared_ptr 的方法
std::shared_ptr<SelfReferencing> getSharedPtr() {
return shared_from_this();
}
// 模拟一个需要将自身传递给其他对象或注册为观察者的场景
void registerMyself(std::vector<std::shared_ptr<SelfReferencing>>& registry) {
registry.push_back(shared_from_this()); // 安全地将自身作为 shared_ptr 添加
}
};
void test_enable_shared_from_this() {
std::shared_ptr<SelfReferencing> obj = std::make_shared<SelfReferencing>("Instance1");
std::cout << "obj use_count: " << obj.use_count() << std::endl; // 1
std::shared_ptr<SelfReferencing> another_obj_ptr = obj->getSharedPtr(); // 获取自身 shared_ptr
std::cout << "obj use_count after getSharedPtr: " << obj.use_count() << std::endl; // 2
std::cout << "another_obj_ptr use_count: " << another_obj_ptr.use_count() << std::endl; // 2
std::vector<std::shared_ptr<SelfReferencing>> global_registry;
obj->registerMyself(global_registry); // 注册自身
std::cout << "obj use_count after registerMyself: " << obj.use_count() << std::endl; // 3
std::cout << "global_registry size: " << global_registry.size() << std::endl;
// 模拟错误用法 (不要这样做!)
// std::shared_ptr<SelfReferencing> bad_ptr(obj.get()); // 危险!会导致二次析构
// std::cout << "Bad ptr use_count: " << bad_ptr.use_count() << std::endl; // 1 (独立计数)
} // obj, another_obj_ptr, global_registry 中的 shared_ptr 离开作用域,SelfReferencing 对象被正确销毁一次
int main() {
test_enable_shared_from_this();
return 0;
}
注意事项:
- 只能在已经由
shared_ptr管理的对象上调用shared_from_this()。如果在原始指针或unique_ptr管理的对象上调用,或者在构造函数中调用,会导致未定义行为 (通常是bad_weak_ptr异常)。 - 在构造函数中,
shared_from_this()尚未初始化,因此不能在构造函数中调用它。如果需要在构造函数中获取shared_ptr,可以考虑使用工厂模式。
多线程环境下的智能指针
- 引用计数是线程安全的:
shared_ptr的引用计数 (强引用和弱引用) 的增减操作都是原子操作,因此在多线程环境下是线程安全的。多个线程可以同时持有、拷贝或销毁shared_ptr而不会破坏引用计数的完整性。 - 指向的对象不是线程安全的: 尽管引用计数是线程安全的,但
shared_ptr所指向的对象本身的数据成员的访问并不是线程安全的。如果多个线程同时读写同一个shared_ptr指向的对象,你仍然需要使用互斥量 (mutex) 或其他同步机制来保护对象的状态。 shared_ptr拷贝不是原子操作: 虽然引用计数的修改是原子的,但shared_ptr对象的拷贝操作 (std::shared_ptr<T> p2 = p1;) 整体而言不是原子的。这意味着,如果一个线程正在拷贝p1,而另一个线程同时reset(p1),可能会导致p2在拷贝时得到一个悬空指针。在多线程中对同一个shared_ptr实例进行修改 (例如赋值、reset()) 需要外部同步。然而,对不同shared_ptr实例的访问 (即使它们指向同一个对象) 则是安全的。
自定义删除器
shared_ptr 和 unique_ptr 都支持自定义删除器。这允许智能指针管理不仅仅是 new 分配的内存,还可以管理其他类型的资源,如文件句柄、数据库连接、动态链接库等。
#include <iostream>
#include <memory>
#include <cstdio> // for std::fclose
// 自定义文件删除器
void file_deleter(FILE* fp) {
if (fp) {
std::fclose(fp);
std::cout << "File closed by custom deleter." << std::endl;
}
}
void test_custom_deleter() {
// 使用 lambda 表达式作为删除器
std::shared_ptr<FILE> file_ptr(std::fopen("test.txt", "w"),
[](FILE* fp) {
if (fp) {
std::fclose(fp);
std::cout << "File closed by lambda deleter." << std::endl;
}
});
if (file_ptr) {
std::fprintf(file_ptr.get(), "Hello from custom deleter!n");
}
// 使用函数指针作为删除器
std::shared_ptr<FILE> file_ptr2(std::fopen("test2.txt", "w"), file_deleter);
if (file_ptr2) {
std::fprintf(file_ptr2.get(), "Hello from function pointer deleter!n");
}
} // file_ptr 和 file_ptr2 离开作用域,自定义删除器被调用
int main() {
test_custom_deleter();
return 0;
}
自定义删除器与循环引用问题没有直接关联,但它是 shared_ptr 灵活性的一个重要体现。
静态成员与智能指针
如果一个类中存在静态 shared_ptr 成员,其生命周期是整个程序运行期间。如果这个静态 shared_ptr 引用了动态创建的对象,并且该动态对象又反过来强引用了与该静态 shared_ptr 相关的其他对象,则可能间接形成一种“静态生命周期”的循环,阻止动态对象的销毁。
通常,静态 shared_ptr 应该谨慎使用,并确保其引用的对象不会参与到可能导致循环引用的复杂对象图中。
故障排除与调试技巧
当程序出现内存泄漏或异常崩溃时,如果怀疑是智能指针问题,可以采取以下调试策略:
-
使用
use_count()观察:
在关键代码路径上,打印shared_ptr::use_count()的值。如果一个对象应该被销毁,但其use_count()始终大于 0,那么很可能存在循环引用或其他生命周期管理问题。std::shared_ptr<MyObject> obj = std::make_shared<MyObject>(); std::cout << "Initial use_count: " << obj.use_count() << std::endl; // ... 传递 obj 给其他 shared_ptr ... std::cout << "Current use_count: " << obj.use_count() << std::endl; -
析构函数日志:
在所有可能被shared_ptr管理的类的析构函数中加入打印语句。如果一个对象应该被销毁但其析构函数没有被调用,那么它就是泄漏的。这是排查内存泄漏最直观有效的方法。class LeakyObject { public: LeakyObject() { std::cout << "LeakyObject constructed." << std::endl; } ~LeakyObject() { std::cout << "LeakyObject destructed!" << std::endl; } // 添加此行 }; -
内存泄漏检测工具:
专业的内存泄漏检测工具是诊断复杂问题的利器。- Valgrind (Linux): 功能强大的动态分析工具,可以检测内存泄漏、内存访问错误等。使用
valgrind --leak-check=full ./your_program运行。 - ASan (AddressSanitizer): Clang/GCC 编译器内置的运行时内存错误检测工具,可以检测内存泄漏、堆栈溢出、使用已释放内存等问题。通过编译选项
-fsanitize=address启用。 - Visual Studio Diagnostic Tools (Windows): Visual Studio IDE 内置了强大的诊断工具,包括内存使用分析器,可以帮助检测和定位内存泄漏。
- Dr. Memory: 跨平台的免费内存调试器。
- Valgrind (Linux): 功能强大的动态分析工具,可以检测内存泄漏、内存访问错误等。使用