智能指针
首先理解什么是RAII,RAII(Resource Acquisition Is Initialization)是C++中用来管理资源的生命周期的一种技术。
在 RAII 中,资源的获取和释放是在对象的构造函数和析构函数中完成的。当对象被创建时,它的构造函数被调用,从而获取资源;当对象超出作用域时,其析构函数被调用,从而释放资源。这样就确保了资源在对象的生命周期内始终是有效的,即使在发生异常或其他错误的情况下也能正确地释放资源。
不带引用计数的智能指针
下面实现一个非常简单的智能指针
#include <iostream> #include <stdexcept> template <typename T> class SmartPointer { public: SmartPointer(T *ptr) : _ptr(ptr) {} ~SmartPointer() { delete _ptr; } T *operator->() { return _ptr; } T &operator*() { return *_ptr; } private: T *_ptr; }; class Bar { public: ~Bar() { std::cout << "destructor" << std::endl; } void func() { std::cout << "func()" << std::endl; } }; int main() { { SmartPointer<Bar> f(new Bar()); f->func(); (*f).func(); } }
代码的运行结果如下:
func()
func()
destructor
可以看到,这个智能指针非常简单,就是在析构函数中正确释放资源,然后提供了->
和*
运算符重载函数。
智能指针就是对普通指针进行了一层封装,然后提供了对应的运算符重载函数,使其接口用起来像普通指针,同时在智能指针对象出作用域时会自动调用析构函数。
只要保证析构函数中,正确释放了资源,那么就能保证资源在出作用域时自动释放。
一般不将智能指针创建在堆上,创建在堆上
SmartPointer<Bar> *f = new SmartPointer<Bar>(new Bar());
,仍需要手动delete,才能访问智能指针的析构函数从而释放资源。这种做法与直接使用裸指针无异。
带引用计数的智能指针
上面的版本,不带引用计数,所以有一个很严重的问题。
int main() { { SmartPointer<Bar> sp(new Bar()); SmartPointer<Bar> sp2(sp); // 拷贝构造 } }
上面这个代码片段的执行结果为:
destructor
destructor
free(): double free detected in tcache 2
Aborted (core dumped)
可以发现,程序对同一个内存执行了两次delete。这是因为,SmartPointer<Bar> sp2(sp);
执行的是浅拷贝,故两个智能指针对象中存放的是同一个内存地址,所以会有double free。
那么,能不能把这个浅拷贝改成深拷贝呢?
答案显然是不能的,改成深拷贝意味着每次执行拷贝都会重新开辟内存空间,然后拷贝数据,但本来的意图可能只是需要传递一个指针而已,也就是两个指针指向同一个内存。
那么,如何解决这个问题呢?从是否共享所有权的角度来分类,有两种办法:
- 不共享所有权:
- 拒绝拷贝的发生,直接把拷贝(移动)构造,拷贝(移动)赋值函数删除,对应的是
std::scoped_ptr
。 - 每次拷贝都是移动,都把资源的所有权转移出去,也就是只有最后一个智能指针拥有所有权,其它智能指针中都置为nullptr,对应的是
std::auto_ptr
。 - 允许移动构造、移动赋值操作,不允许拷贝构造、拷贝赋值,也就是
c++11
中的std::unique_ptr
,通过std::unique_ptr<T> uptr2(std::move(uptr1))
来创建新对象。
- 拒绝拷贝的发生,直接把拷贝(移动)构造,拷贝(移动)赋值函数删除,对应的是
std::scoped_ptr
只能用于单个对象,不能拷贝移动,std::auto_ptr
自动转移所有权,容易出错,比如再次使用交出所有权的对象,这俩个都不推荐使用。
std::unique_ptr
可以移动构造新对象,而且是显式的发生所有权的转移的。
- 共享所有权:
利用引用计数,通过引用计数是否为0,来判断是否真的执行delete操作。当发生拷贝时,引用计数加一,当有一个智能指针出作用域,对应的引用计数减一,当引用计数减为0时,才真正执行delete操作。
这样就能保证,不发生double free,同时最后一个出作用域的智能指针能够正确释放资源。
尝试实现一个带引用技术的智能指针
#include <iostream> #include <stdexcept> template <typename T> class RefCnt { public: RefCnt(T *ptr) : ptr(ptr) { if (ptr) cnt = 1; } void increRef() { cnt++; } int decreRef() { return --cnt; } private: T *ptr; int cnt; // 不是线程安全的 }; template <typename T> class SharedPtr { public: SharedPtr(T *ptr) : ptr(ptr) { rc = new RefCnt<T>(ptr); } SharedPtr(const SharedPtr &sp) : ptr(sp.ptr), rc(sp.rc) { if (ptr) rc->increRef(); } SharedPtr &operator=(const SharedPtr &sp) { if (this == &sp) return *this; if (0 == rc->decreRef()) { delete ptr; } ptr = sp.ptr; rc = sp.rc; rc->increRef(); return *this; } ~SharedPtr() { if (0 == rc->decreRef()) { delete ptr; delete rc; } } T *operator->() { return ptr; } T &operator*() { return *ptr; } private: T *ptr; RefCnt<T> *rc; }; int main() { SharedPtr<int> sp1(new int(10)); SharedPtr<int> sp2(sp1); SharedPtr<int> sp3(nullptr); sp3 = sp2; *sp1 = 42; std::cout << *sp2 << " " << *sp3 << std::endl; }
shared_ptr,weak_ptr
shared_ptr: 强智能指针 可以改变资源的引用计数
weak_ptr:弱智能指针 不会改变资源的引用计数(一般用于解决强智能指针的循环引用问题,以及多线程环境下访问共享对象的线程安全问题)
shared_ptr
:
shared_ptr
用于共享资源的所有权,可以有多个shared_ptr
指向相同的内存资源。- 每当有一个新的
shared_ptr
指向对象时,该对象的引用计数会增加。(通过将已有的shared_ptr
拷贝或者赋值给新的shared_ptr
对象) - 当引用计数为零时,即没有任何
shared_ptr
指向对象时,对象会被自动销毁。
weak_ptr
:
weak_ptr
可以由shared_ptr
构造而来,如std::weak_ptr<int> weak = shared;
。weak_ptr
用于解决shared_ptr
的循环引用问题。它不会增加引用计数,因此不会影响对象的生命周期。weak_ptr
允许你观察一个shared_ptr
指向的对象,但不拥有它。如果所有指向对象的shared_ptr
都被销毁,weak_ptr
会自动失效,避免悬挂指针问题。weak_ptr
可以通过lock()
方法转换为一个shared_ptr
,如果被观察的对象仍然存在,就返回一个有效的shared_ptr
;如果对象已被销毁,则返回一个空的shared_ptr
。
weak_ptr
可以用于在多线程环境下,对不拥有所有权的资源的安全访问,通过这种弱引用,能够保证,不影响被观察的对象的生命周期,同时又能观察到该目标。
而如果用shared_ptr
则会修改目标对象的生命周期。
#include <chrono> #include <iostream> #include <memory> #include <thread> void worker(std::weak_ptr<int> weak) { std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟一段时间后观察 if (auto shared = weak.lock()) { std::cout << "shared_ptr is still valid. Value: " << *shared << std::endl; } else { std::cout << "shared_ptr has been released." << std::endl; } } int main() { std::shared_ptr<int> shared = std::make_shared<int>(42); std::thread observer(worker, std::weak_ptr<int>(shared)); std::this_thread::sleep_for(std::chrono::seconds(3)); shared.reset(); observer.join(); return 0; }
weak_ptr
还能用于解决shared_ptr
导致的循环应用问题
#include <iostream> #include <memory> class B; // 前置声明 class A { public: A() { std::cout << "A constructor\n"; } ~A() { std::cout << "A destructor\n"; } void setB(std::shared_ptr<B> b) { m_b = b; } private: std::shared_ptr<B> m_b; }; class B { public: B() { std::cout << "B constructor\n"; } ~B() { std::cout << "B destructor\n"; } void setA(std::weak_ptr<A> a) { m_a = a; } private: std::weak_ptr<A> m_a; }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); // 创建循环引用 a->setB(b); b->setA(a); // 释放智能指针 a.reset(); b.reset(); // 此时 A 和 B 对象会正确地被销毁,避免了循环引用导致的内存泄漏 return 0; }
自定义删除器
使用 std::shared_ptr
或 std::unique_ptr
来管理动态分配的内存时,可以使用自定义的删除器(deleter)来指定如何释放资源,这在编写各种线程池、连接池、文件资源管理(使用自定义删除器来调用 fclose 关闭文件)时比较常用。
void custom_deleter(int* ptr) { std::cout << "Custom deleter called\n"; delete ptr; } int main() { int* raw_ptr = new int(42); // 当然,除了使用函数指针,还可以使用函数对象或者lambda来实现。 std::shared_ptr<int> ptr(raw_ptr, custom_deleter); return 0; }
本文来自博客园,作者:EricLing0529,转载请注明原文链接:https://www.cnblogs.com/ericling0529/p/18156564
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?