C++智能指针:shared_ptr、unique_ptr、weak_ptr区别及应用场景
智能指针是C++中用于管理动态分配内存的重要工具,解决了传统裸指针容易导致的内存泄漏、悬空指针等问题。C++11引入了std::shared_ptr、std::unique_ptr和std::weak_ptr,它们分别适用于不同的场景。本文将详细介绍这三种智能指针的特点、应用场景,并通过代码示例展示其功能。
一、std::shared_ptr
1.1 介绍
std::shared_ptr是一种共享所有权的智能指针,允许多个指针共享同一个动态分配的对象。它通过引用计数(reference counting)机制管理内存,当最后一个shared_ptr销毁或重置时,对象才会被删除。
1.2 特点
- 共享所有权:多个shared_ptr可以指向同一对象,引用计数跟踪共享指针数量。
- 自动内存管理:当引用计数为0时,自动释放对象。
- 线程安全计数:引用计数的增减操作是线程安全的,但对象本身的访问需用户自行保证。
- 自定义删除器:支持用户定义的删除器,用于特殊资源管理。
- 开销:由于维护引用计数,shared_ptr相比unique_ptr有额外的内存和性能开销。
1.3 应用场景
- 需要多个指针共享同一资源,如对象在多个模块间传递。
- 动态分配的资源生命周期不明确,需由引用计数决定。
- 管理需要共享的复杂数据结构,如树或图。
1.4 代码示例
以下是一个使用std::shared_ptr管理共享资源的示例,展示其基本用法和引用计数管理。
#include <iostream>
#include <memory>
#include <string>
class Resource {
public:
Resource(const std::string& name) : name_(name) {
std::cout << "Resource " << name_ << " created\n";
}
~Resource() {
std::cout << "Resource " << name_ << " destroyed\n";
}
void use() const {
std::cout << "Using resource " << name_ << "\n";
}
private:
std::string name_;
};
void sharedPtrDemo() {
// 创建shared_ptr
std::shared_ptr<Resource> res1 = std::make_shared<Resource>("R1");
std::cout << "Reference count: " << res1.use_count() << "\n";
// 共享所有权
{
std::shared_ptr<Resource> res2 = res1;
std::cout << "Reference count after res2: " << res1.use_count() << "\n";
res2->use();
} // res2离开作用域,引用计数减1
std::cout << "Reference count after res2 destroyed: " << res1.use_count() << "\n";
res1->use();
} // res1离开作用域,资源销毁
运行结果:
Resource R1 created
Reference count: 1
Reference count after res2: 2
Using resource R1
Reference count after res2 destroyed: 1
Using resource R1
Resource R1 destroyed
在这个示例中,std::make_shared创建了一个shared_ptr,通过use_count()可以查看当前引用计数。res2共享了res1的资源,引用计数增至2,当res2销毁后,引用计数减为1,最终res1销毁时资源被释放。
二、std::unique_ptr
2.1 介绍
std::unique_ptr是独占所有权的智能指针,同一时刻只有一个unique_ptr可以指向某个对象。它轻量高效,适合需要明确所有权的场景。
2.2 特点
- 独占所有权:不能复制,只能通过std::move转移所有权。
- 轻量高效:没有引用计数,内存和性能开销低。
- 自动释放:离开作用域时自动删除对象。
- 自定义删除器:支持自定义删除器,但需在类型中指定。
- 空指针安全:支持空指针状态,析构时无操作。
2.3 应用场景
- 需要独占资源,避免共享带来的复杂性。
- 作为函数返回值,传递动态分配的对象所有权。
- 管理临时资源,确保资源在特定作用域内释放。
2.4 代码示例
以下示例展示了std::unique_ptr的独占所有权和所有权转移。
#include <iostream>
#include <memory>
#include <string>
class Resource {
public:
Resource(const std::string& name) : name_(name) {
std::cout << "Resource " << name_ << " created\n";
}
~Resource() {
std::cout << "Resource " << name_ << " destroyed\n";
}
void use() const {
std::cout << "Using resource " << name_ << "\n";
}
private:
std::string name_;
};
std::unique_ptr<Resource> createResource(const std::string& name) {
return std::make_unique<Resource>(name); // 返回unique_ptr
}
void uniquePtrDemo() {
// 创建unique_ptr
std::unique_ptr<Resource> res1 = createResource("U1");
res1->use();
// 转移所有权
std::unique_ptr<Resource> res2 = std::move(res1);
if (!res1) {
std::cout << "res1 is null after move\n";
}
res2->use();
// 自定义删除器示例
auto deleter = [](Resource* ptr) {
std::cout << "Custom deleter called\n";
delete ptr;
};
std::unique_ptr<Resource, decltype(deleter)> res3(new Resource("U2"), deleter);
res3->use();
} // 离开作用域,资源自动销毁
运行结果:
Resource U1 created
Using resource U1
res1 is null after move
Using resource U1
Resource U2 created
Using resource U2
Custom deleter called
Resource U2 destroyed
Resource U1 destroyed
此示例中,std::make_unique创建unique_ptr,通过std::move转移所有权后,res1变为空指针。还展示了自定义删除器的用法,适合特殊资源管理。
三、std::weak_ptr
3.1 介绍
std::weak_ptr是一种非拥有型智能指针,通常与std::shared_ptr配合使用。它不控制对象的生命周期,仅提供对shared_ptr管理对象的访问。
3.2 特点
- 不拥有资源:不会增加shared_ptr的引用计数。
- 检测悬空指针:通过expired()检查资源是否已被销毁。
- 转换为shared_ptr:通过lock()获取shared_ptr以访问对象。
- 避免循环引用:解决shared_ptr可能导致的循环引用问题。
3.3 应用场景
- 打破shared_ptr的循环引用,如在树或图结构中。
- 缓存或观察资源,需检查资源是否有效。
- 临时访问共享资源,不影响其生命周期。
3.4 代码示例
以下示例展示std::weak_ptr如何避免循环引用和检查资源有效性。
#include <iostream>
#include <memory>
#include <string>
class Node {
public:
std::string name_;
std::shared_ptr<Node> partner_;
std::weak_ptr<Node> weak_partner_; // 使用weak_ptr避免循环引用
Node(const std::string& name) : name_(name) {
std::cout << "Node " << name_ << " created\n";
}
~Node() {
std::cout << "Node " << name_ << " destroyed\n";
}
};
void weakPtrDemo() {
// 创建两个共享节点
std::shared_ptr<Node> node1 = std::make_shared<Node>("N1");
std::shared_ptr<Node> node2 = std::make_shared<Node>("N2");
// 设置循环引用
node1->partner_ = node2;
node2->partner_ = node1; // 循环引用,可能导致内存泄漏
// 使用weak_ptr避免循环引用
node1->weak_partner_ = node2;
node2->weak_partner_ = node1;
// 检查weak_ptr
if (auto temp = node1->weak_partner_.lock()) {
std::cout << "node1's weak_partner: " << temp->name_ << "\n";
} else {
std::cout << "node1's weak_partner is expired\n";
}
// 释放node2
node2.reset();
if (node1->weak_partner_.expired()) {
std::cout << "node1's weak_partner is expired after node2 reset\n";
}
}
运行结果:
Node N1 created
Node N2 created
node1's weak_partner: N2
Node N2 destroyed
node1's weak_partner is expired after node2 reset
Node N1 destroyed
此示例中,partner_形成了循环引用,导致内存泄漏(若无其他干预)。而weak_partner_使用weak_ptr,不会增加引用计数,避免了循环引用问题。通过lock()和expired()检查资源状态,确保安全访问。
四、比较与选择
特性 | shared_ptr | unique_ptr | weak_ptr |
所有权 | 共享 | 独占 | 无 |
引用计数 | 有 | 无 | 无 |
复制 | 支持 | 不支持(仅移动) | 支持 |
性能开销 | 较高(引用计数) | 较低 | 中等 |
典型场景 | 共享资源、复杂数据结构 | 独占资源、函数返回值 | 循环引用、资源观察 |
选择建议:
- 如果资源需要独占,选择unique_ptr,性能更高且逻辑清晰。
- 如果资源需要共享且生命周期复杂,选择shared_ptr。
- 如果需要访问shared_ptr管理的资源但不控制生命周期,或需打破循环引用,选择weak_ptr。
五、注意事项
- 避免裸指针:不要将同一裸指针传递给多个智能指针,可能导致多次删除。
- 优先使用make函数:std::make_shared和std::make_unique更安全,减少内存分配次数。
- 循环引用:shared_ptr可能导致循环引用,需用weak_ptr解决。
- 线程安全:shared_ptr的引用计数线程安全,但对象访问需加锁保护。
六、总结
std::shared_ptr、std::unique_ptr和std::weak_ptr各有其独特的设计理念和应用场景。shared_ptr适合共享资源,unique_ptr强调独占所有权,weak_ptr则是解决循环引用和临时访问的利器。通过合理选择智能指针,开发者可以编写更安全、高效的C++代码。