1. 智能指针概述:从手动管理到自动管理
在C++的世界里,内存管理一直是个让人又爱又恨的话题。爱的是它给了我们极大的控制权,恨的是稍不留神就会导致内存泄漏或悬空指针。传统的new和delete就像手动挡汽车,虽然灵活但需要驾驶员全神贯注。而智能指针则像是自动挡,让我们可以更专注于驾驶本身而非换挡操作。
C++11引入了三种主要的智能指针:unique_ptr、shared_ptr和weak_ptr。它们各自有不同的所有权语义:
unique_ptr:独占所有权,轻量高效shared_ptr:共享所有权,引用计数weak_ptr:观察者模式,不增加引用计数
今天我们要重点探讨的是shared_ptr和它的好搭档weak_ptr,以及如何通过定制删除器来扩展它们的功能。
2. shared_ptr的线程安全性分析
shared_ptr的线程安全性是个经常被误解的话题。很多人以为用了shared_ptr就万事大吉了,其实不然。让我们先看一个简单的例子:
#include <iostream>
#include <memory>
#include <thread>
// 技术栈:C++11及以上
void thread_func(std::shared_ptr<int> sp) {
// 线程内使用shared_ptr是安全的
std::cout << "Thread count: " << sp.use_count() << std::endl;
}
int main() {
auto sp = std::make_shared<int>(42);
std::thread t1(thread_func, sp);
std::thread t2(thread_func, sp);
std::cout << "Main count: " << sp.use_count() << std::endl;
t1.join();
t2.join();
return 0;
}
在这个例子中,多个线程同时持有同一个shared_ptr的副本是安全的。但是,如果多个线程同时修改同一个shared_ptr对象(不是副本),那就需要额外的同步机制了。
2.1 shared_ptr的线程安全规则
引用计数的修改是原子的:
shared_ptr的引用计数增减操作是线程安全的,这保证了对象只会在最后一个引用消失时被销毁。控制块是线程安全的:
shared_ptr的控制块(包含引用计数等元数据)的修改是线程安全的。指向的数据不自动保证线程安全:
shared_ptr只管理指针的生命周期,不保证指向的数据的线程安全性。同一个shared_ptr对象的读写需要同步:如果多个线程要读写同一个
shared_ptr对象(不是副本),需要使用锁或其他同步机制。
2.2 不安全的shared_ptr使用示例
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
// 不安全的shared_ptr使用示例
std::shared_ptr<int> global_sp = std::make_shared<int>(0);
void unsafe_increment() {
for (int i = 0; i < 10000; ++i) {
// 多个线程同时修改同一个shared_ptr对象
global_sp = std::make_shared<int>(*global_sp + 1);
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(unsafe_increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final value: " << *global_sp << std::endl;
return 0;
}
这个例子展示了多个线程同时修改同一个shared_ptr对象导致的数据竞争问题。运行结果通常不会是我们期望的100000。
2.3 安全的shared_ptr多线程使用方式
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
// 安全的shared_ptr多线程使用
std::shared_ptr<int> global_sp = std::make_shared<int>(0);
std::mutex mtx;
void safe_increment() {
for (int i = 0; i < 10000; ++i) {
std::shared_ptr<int> local_sp;
{
std::lock_guard<std::mutex> lock(mtx);
local_sp = global_sp; // 在锁保护下获取副本
}
// 在锁外使用副本是安全的
auto new_val = std::make_shared<int>(*local_sp + 1);
{
std::lock_guard<std::mutex> lock(mtx);
global_sp = new_val; // 在锁保护下更新
}
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(safe_increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final value: " << *global_sp << std::endl;
return 0;
}
这个改进版本使用了互斥锁来保护对global_sp的访问,确保了线程安全。
3. weak_ptr:解决shared_ptr循环引用问题
weak_ptr是shared_ptr的观察者,它不会增加引用计数,主要用于解决shared_ptr的循环引用问题。让我们看一个典型的使用场景:
#include <iostream>
#include <memory>
class Node {
public:
std::string name;
std::shared_ptr<Node> parent;
std::shared_ptr<Node> child;
Node(const std::string& n) : name(n) {
std::cout << "Node " << name << " created" << std::endl;
}
~Node() {
std::cout << "Node " << name << " destroyed" << std::endl;
}
};
void circular_reference_example() {
auto node1 = std::make_shared<Node>("Parent");
auto node2 = std::make_shared<Node>("Child");
node1->child = node2;
node2->parent = node1;
// 即使离开作用域,node1和node2也不会被销毁
// 因为它们的引用计数永远不会降到0
}
int main() {
circular_reference_example();
std::cout << "End of circular_reference_example" << std::endl;
return 0;
}
运行这个例子,你会发现析构函数永远不会被调用,这就是典型的循环引用导致的内存泄漏。
3.1 使用weak_ptr解决循环引用
#include <iostream>
#include <memory>
class NodeFixed {
public:
std::string name;
std::weak_ptr<NodeFixed> parent; // 使用weak_ptr替代shared_ptr
std::shared_ptr<NodeFixed> child;
NodeFixed(const std::string& n) : name(n) {
std::cout << "Node " << name << " created" << std::endl;
}
~NodeFixed() {
std::cout << "Node " << name << " destroyed" << std::endl;
}
void printParent() {
if (auto p = parent.lock()) { // 尝试获取shared_ptr
std::cout << name << "'s parent is " << p->name << std::endl;
} else {
std::cout << name << "'s parent is expired" << std::endl;
}
}
};
void weak_ptr_solution() {
auto node1 = std::make_shared<NodeFixed>("Parent");
auto node2 = std::make_shared<NodeFixed>("Child");
node1->child = node2;
node2->parent = node1; // 这里使用weak_ptr,不会增加引用计数
node2->printParent();
// 离开作用域时,node1和node2都会被正确销毁
}
int main() {
weak_ptr_solution();
std::cout << "End of weak_ptr_solution" << std::endl;
return 0;
}
在这个改进版本中,我们使用weak_ptr来打破循环引用,内存现在可以被正确释放了。
3.2 weak_ptr的过期检查
weak_ptr提供了expired()方法来检查关联的shared_ptr是否还存在,以及lock()方法来尝试获取一个shared_ptr。看一个更复杂的例子:
#include <iostream>
#include <memory>
#include <vector>
class Observer {
public:
virtual void update() = 0;
virtual ~Observer() = default;
};
class Subject {
std::vector<std::weak_ptr<Observer>> observers;
public:
void addObserver(std::weak_ptr<Observer> obs) {
observers.push_back(obs);
}
void notify() {
auto it = observers.begin();
while (it != observers.end()) {
if (auto obs = it->lock()) {
obs->update();
++it;
} else {
// 观察者已失效,从列表中移除
it = observers.erase(it);
}
}
}
};
class ConcreteObserver : public Observer {
int id;
public:
ConcreteObserver(int i) : id(i) {}
void update() override {
std::cout << "Observer " << id << " notified" << std::endl;
}
};
int main() {
Subject subject;
{
auto obs1 = std::make_shared<ConcreteObserver>(1);
subject.addObserver(obs1);
{
auto obs2 = std::make_shared<ConcreteObserver>(2);
subject.addObserver(obs2);
subject.notify(); // 两个观察者都会收到通知
} // obs2离开作用域被销毁
subject.notify(); // 只有obs1会收到通知
} // obs1离开作用域被销毁
subject.notify(); // 没有观察者会收到通知
return 0;
}
这个例子展示了weak_ptr在观察者模式中的应用,它可以自动处理观察者对象的生命周期问题。
4. 定制删除器:扩展shared_ptr的功能
shared_ptr允许我们自定义删除器,这在管理非传统资源时特别有用。默认情况下,shared_ptr会使用delete来释放内存,但我们可以改变这一行为。
4.1 基本定制删除器示例
#include <iostream>
#include <memory>
#include <cstdio>
void file_deleter(FILE* fp) {
if (fp) {
std::cout << "Closing file..." << std::endl;
fclose(fp);
}
}
int main() {
// 使用自定义删除器管理文件句柄
std::shared_ptr<FILE> sp(
fopen("test.txt", "w"),
file_deleter // 指定自定义删除器
);
if (sp) {
fprintf(sp.get(), "Hello, World!");
}
// 文件会在sp离开作用域时自动关闭
return 0;
}
4.2 更复杂的删除器示例
#include <iostream>
#include <memory>
#include <functional>
class Resource {
public:
Resource() { std::cout << "Resource acquired" << std::endl; }
~Resource() { std::cout << "Resource released" << std::endl; }
void use() { std::cout << "Using resource" << std::endl; }
};
void custom_deleter(Resource* res) {
std::cout << "Custom deleter called" << std::endl;
delete res;
}
int main() {
// 使用函数指针作为删除器
std::shared_ptr<Resource> sp1(new Resource, custom_deleter);
// 使用lambda表达式作为删除器
std::shared_ptr<Resource> sp2(
new Resource,
[](Resource* res) {
std::cout << "Lambda deleter called" << std::endl;
delete res;
}
);
// 使用std::function作为删除器
std::function<void(Resource*)> deleter = [](Resource* res) {
std::cout << "std::function deleter called" << std::endl;
delete res;
};
std::shared_ptr<Resource> sp3(new Resource, deleter);
sp1->use();
sp2->use();
sp3->use();
return 0;
}
4.3 删除器在数组上的应用
#include <iostream>
#include <memory>
int main() {
// 管理数组的shared_ptr需要自定义删除器
std::shared_ptr<int> sp(
new int[10],
[](int* p) {
std::cout << "Deleting array" << std::endl;
delete[] p;
}
);
for (int i = 0; i < 10; ++i) {
sp.get()[i] = i * i;
}
// C++17及以上可以使用shared_ptr管理数组的更好方式
auto sp17 = std::make_shared<int[]>(10);
for (int i = 0; i < 10; ++i) {
sp17[i] = i * i;
}
return 0;
}
5. 应用场景与最佳实践
5.1 shared_ptr的应用场景
- 共享所有权:当多个对象需要共享同一个资源时。
- 工厂模式:工厂方法返回对象的所有权给调用者。
- 缓存系统:需要自动释放不再使用的缓存项。
- 观察者模式:如我们前面看到的weak_ptr示例。
5.2 weak_ptr的应用场景
- 打破循环引用:如父子节点、双向链表等场景。
- 缓存系统:存储对缓存项的弱引用。
- 观察者模式:主题不需要控制观察者的生命周期。
- 临时对象访问:需要临时访问可能已被释放的资源。
5.3 定制删除器的应用场景
- 管理非内存资源:如文件句柄、网络连接、数据库连接等。
- 特殊内存分配:如内存池、对齐内存等。
- 调试和日志:在释放资源时记录日志。
- 数组管理:如前面所示的管理动态数组。
5.4 注意事项
- 避免裸指针转换:不要将裸指针转换为shared_ptr,这可能导致多次删除。
- 优先使用make_shared:它更高效,能减少内存分配次数。
- 循环引用:注意潜在的循环引用问题,及时使用weak_ptr。
- 性能开销:shared_ptr比unique_ptr有额外的性能开销。
- 线程安全:记住shared_ptr本身的线程安全规则。
6. 总结
C++智能指针是现代C++编程中不可或缺的工具,它们极大地简化了资源管理,减少了内存泄漏的风险。shared_ptr提供了共享所有权的能力,weak_ptr则解决了循环引用问题,而定制删除器则扩展了智能指针的功能,使其能够管理各种类型的资源。
在实际开发中,我们应该:
- 优先使用
make_shared创建shared_ptr - 在可能产生循环引用的地方使用
weak_ptr - 根据资源类型选择合适的删除器
- 注意智能指针的线程安全特性
- 避免混合使用智能指针和裸指针
智能指针不是银弹,但它们确实能帮助我们编写更安全、更易维护的代码。理解它们的内部机制和使用场景,可以让我们在合适的场合发挥它们的最大价值。
评论