一、智能指针简介
1.1 为什么需要智能指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void UseRawPointer() { string *str = new string("hello world"); return; }
void UseRawPointer() { string *str = new string("hello world");
delete str;
return; }
|
在使用传统指针时,需要手动分配和释放内存,这可能会导致内存泄漏(未释放已分配的内存)或释放后的野指针(指向已释放的内存)。智能指针通过自动内存管理来帮助解决这些问题,确保在不再需要内存时自动释放它。
1.2 智能指针思想
智能指针是在<memory>头文件中的 std 命名空间中定义的。 将智能指针抽象成一个对象,由该对象管理动态申请的内存资源,当该对象出作用域析构时,如果引用计数减到0,则连着将动态申请的内存free。
实现智能指针关键点
RAII 是 “资源获取即初始化”(Resource Acquisition Is Initialization)的缩写,旨在通过对象生命周期的管理来确保资源(如内存、文件句柄、数据库连接等)的正确获取和释放。RAII 的核心思想是,资源的获取操作应该与对象的初始化操作绑定在一起,资源的释放应该与对象的销毁操作绑定在一起。这意味着在对象的构造函数中获取资源,在对象的析构函数中释放资源,从而确保资源的正确管理。
RAII 有以下关键特点:
- 资源管理封装: RAII 鼓励将资源的管理和使用封装在对象中,以确保资源在对象的生命周期内正确获取和释放。这允许开发人员不必手动记住释放资源的细节。
- 自动化资源释放: 当对象超出其作用域范围时,其析构函数自动被调用,从而自动释放与资源相关的资源,无论是通过正常的代码路径还是异常路径。
- 异常安全性: 使用 RAII 可以确保即使在发生异常时,资源也会被正确释放,从而避免资源泄漏。
- 代码可读性: RAII 使代码更加清晰和易于理解,因为资源管理逻辑被封装在对象的构造和析构中,而不是分散在代码中。
二、auto_ptr
2.1 面临问题
auto_ptr 是c++ 98定义的智能指针模板,C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代。
auto_ptr 被C++11抛弃的主要原因
拷贝构造或者赋值采用资源管理器转移思想,存在被拷贝对象悬空问题
1 2 3 4 5
| auto_ptr<Date> d1(new Date(2023,10,1)); auto_ptr<Date> d2(new Date(2023,11,11));
d1 = d2; cout << d2.get() << endl;
|
在STL中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| vector<auto_ptr<string>> v; auto_ptr<string> s1(new string("I'm s1")); auto_ptr<string> s2(new string("I'm s2"));
v.push_back(std::move(s1)); v.push_back(std::move(s2));
cout << "v[0]:" << *v[0] << endl; cout << "v[1]:" << *v[1] << endl;
v[0] = v[1]; cout << "v[0]:" << *v[0] << endl; cout << "v[1]:" << *v[1] << endl;
|
不支持对象数组的内存管理
1
| auto_ptr<int[]> arr(new int[5]);
|
2.2 模拟实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| template <class T> class auto_ptr{ private: T* _ptr; public: explicit auto_ptr(T *p = 0) : _ptr(p) {}
auto_ptr(auto_ptr &a) : _ptr(a.release()) {}
auto_ptr &operator=(auto_ptr &__p) { reset(__p.release()); return *this; }
~auto_ptr() { delete _ptr; }
T *get() const { return _ptr; }
T *release() { T *tmp = _ptr; _ptr = nullptr; return tmp; }
void reset(T *p = 0) { if (_ptr != p) delete _ptr; _ptr = p; }
T &operator*() const { return *_ptr; }
T *operator->() const { return _ptr; } };
|
三、unique_ptr
唯一拥有:unique_ptr 是独占所有权的智能指针,意味着在任何时刻只能有一个 unique_ptr 拥有某个对象的所有权。这也确保了对象不会被多个指针共享,避免了重复释放内存的风险。
自动释放:当 unique_ptr 超出其作用域时,会自动调用 delete 释放所管理的对象。这减少了内存泄漏的风险。
不支持拷贝:unique_ptr 不允许拷贝,防止两个指针指向同一个对象。
支持移动语义:可以通过 std::move 将 unique_ptr 转移到另一个指针,这样原来的指针就失去了对对象的所有权。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| template<typename T> class unique_ptr { private: T *_ptr; public: explicit unique_ptr(T *p = nullptr) : _ptr(p) {} unique_ptr &operator=(const unique_ptr &) = delete;
unique_ptr(const unique_ptr &) = delete;
unique_ptr(unique_ptr &&o) : _ptr(o.release()) {} unique_ptr &operator=(nullptr_t) noexcept { reset(); return *this; }
unique_ptr &operator=(unique_ptr &&o) { if(this != &o){ reset(o.release()); }
return *this; }
T *release() noexcept { T *tmp = _ptr; _ptr = nullptr; return tmp; }
void reset(T *p = nullptr) noexcept { if (_ptr != p) delete _ptr; _ptr = p; } ~unique_ptr() { delete _ptr; }
T *get() const { return _ptr; }
void swap(unique_ptr &x) noexcept { std::swap(this->_ptr, x._ptr); }
T &operator*() const { __glibcxx_assert(get() != nullptr); return *get(); }
T *operator->() const noexcept { __glibcxx_assert(get() != nullptr); return get(); }
explicit operator bool() const noexcept { return nullptr == get() ? false : true; } };
|
四、shared_ptr
共享所有权:多个 shared_ptr 可共享对一个对象的所有权,并共同负责管理该对象的生命周期。当最后一个 shared_ptr 被销毁时,对象的生命周期才会结束。
引用计数:每当创建一个新的 shared_ptr 指向同一对象时,引用计数会增加;每当 shared_ptr 被销毁或重置时,引用计数会减少。当引用计数降为零时,所指向的对象会被销毁。
线程安全:对 shared_ptr 本身的操作是线程安全的,引用计数的增加和减少是线程安全的。对所指对象的操作不一定是线程安全的,如果多个线程需要同时读取或修改由 shared_ptr 所管理的对象,需要自己管理该对象的线程安全,例如使用互斥锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| template<class T> class shared_ptr { public: explicit shared_ptr(T *ptr = nullptr) : _ptr(ptr), _pcount(new int(1)), _pmtx(new mutex) {}
~shared_ptr() { Release(); }
void Release() { _pmtx->lock(); bool deleteFlag = false; if (--(*_pcount) == 0) { if (_ptr) { _del(_ptr); }
delete _pcount; deleteFlag = true; } _pmtx->unlock(); if (deleteFlag) { delete _pmtx; } }
void AddCount() { _pmtx->lock(); ++(*_pcount); _pmtx->unlock(); }
shared_ptr(const shared_ptr<T> &sp) : _ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx) { AddCount(); }
shared_ptr<T> &operator=(const shared_ptr<T> &sp) { if (_ptr != sp._ptr) { Release();
_ptr = sp._ptr; _pcount = sp._pcount; _pmtx = sp._pmtx;
AddCount(); }
return *this; }
T &operator*() { return *_ptr; }
T *operator->() { return _ptr; }
T *get() { return _ptr; }
int use_count() { return *_pcount; }
void swap(shared_ptr<T> &sp) noexcept { std::swap(_ptr, sp._ptr); std::swap(_pcount, sp._pcount); std::swap(_pmtx, sp._pmtx); }
void reset(T *ptr = nullptr) { Release();
_ptr = ptr; _pcount = new int(1); _pmtx = new mutex; }
private: T *_ptr; int *_pcount; mutex *_pmtx;
function<void(T *)> _del = [](T *ptr) { cout << "lambda delete:" << ptr << endl; delete ptr; }; };
|
4.1 函数模版make_shared
shared_ptr 需要维护引用计数的信息:
强引用, 用来记录当前有多少个存活的 shared_ptrs 正共享该对象,共享的对象会在最后一个强引用离开的时候销毁( 也可能释放)
弱引用, 用来记录当前有多少个正在观察该对象的 weak_ptrs.,当最后一个弱引用离开的时候, 共享的内部信息控制块会被销毁和释放 (共享的对象也会被释放, 如果还没有释放的话)。
优点1 提高内存分配效率
通过使用原始的 new运算符分配对象, 然后传递给 shared_ptr (也就是使用 shared_ptr 的构造函数) 的话, shared_ptr 的实现没有办法选择, 而只能单独的分配控制块,总共需要分配2次内存空间。
make_shared 只需要单次内存分配,提高了性能,并减少了内存碎片。
优点2 异常安全
使用 new 直接创建对象时,如果在 std::shared_ptr 构造之前抛出异常,可能会导致内存泄漏。而使用 make_shared 可以避免这种情况,因为对象的分配和智能指针的构造是原子操作,不会中途出现内存泄漏的问题。
缺点1 对象构造函数是保护或私有时,无法使用 make_shared
缺点2 对象的内存可能无法及时回收
make_shared 只分配一次内存,减少了内存分配的开销,使得控制块和托管对象在同一内存块上分配。而控制块是由 shared_ptr 和 weak_ptr 共享的,因此两者共同管理着这个内存块(托管对象 + 控制块)。当强引用计数为 0 时,托管对象被析构(即析构函数被调用),但内存块并未被回收,只有等到最后一个 weak_ptr 离开作用域时,弱引用也减为 0 才会释放这块内存块。原本强引用减为 0 时就可以释放的内存, 现在变为了强引用和弱引用都减为 0 时才能释放, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说,是一个需要注意的问题。
4.2 类模版enable_shared_from_this
enable_shared_from_this是一个模板类,定义于头文件<memory>。 enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 shared_ptr 对象 pt 管理)安全地创建指向自身的 shared_ptr 智能指针(假设名为 pt1, pt2, … ) ,它们与 pt 共享对象 t 的所有权。
若一个类 T 继承enable_shared_from_this<T> ,则会为该类 T 提供成员函数 shared_from_this 。 当 T 类型对象 t 被一个为名为 pt 的 std::shared_ptr 类对象管理时,调用 T::shared_from_this 成员函数,将会返回一个新的 std::shared_ptr 对象,它与 pt 共享 t 的所有权。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Bad { public: std::shared_ptr<Bad> getptr() { return std::shared_ptr<Bad>(this); }
~Bad() { std::cout << "Bad::~Bad() called" << std::endl; } };
int main() { // 错误的示例,每个shared_ptr都认为自己是对象仅有的所有者 std::shared_ptr<Bad> bp1(new Bad()); std::shared_ptr<Bad> bp2 = bp1->getptr(); std::cout << "bp1.use_count() = " << bp1.use_count() << std::endl; // 输出1 std::cout << "bp2.use_count() = " << bp2.use_count() << std::endl; // 输出1 // 进程崩溃,对象析构了2次 }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include <iostream> #include <memory> #include <thread> #include <chrono>
class AsyncWorker : public std::enable_shared_from_this<AsyncWorker> { public: void startAsyncTask() { auto self = shared_from_this(); std::thread([self] { std::this_thread::sleep_for(std::chrono::seconds(2)); self->onTaskComplete(); }).detach(); }
void onTaskComplete() { std::cout << "Task complete!" << std::endl; }
~AsyncWorker() { std::cout << "AsyncWorker destroyed!" << std::endl; } };
int main() { { std::shared_ptr<AsyncWorker> worker = std::make_shared<AsyncWorker>(); worker->startAsyncTask(); std::this_thread::sleep_for(std::chrono::seconds(3)); } std::this_thread::sleep_for(std::chrono::seconds(1)); return 0; }
|
应用场景
安全的引用管理: 如果类中的某些方法需要返回一个 shared_ptr 指向该类的实例,而调用方已经拥有一个 shared_ptr,直接返回 this 会产生问题,因为 this 不是智能指针。如果多个地方都创建 shared_ptr,对象可能会在引用计数不一致的情况下被提前释放。通过 enable_shared_from_this,可以确保同一个对象的所有 shared_ptr 共享相同的引用计数。
异步操作或回调: 在异步操作(如网络请求、I/O 操作、线程回调)中,如果想确保对象在操作完成之前不会被销毁,可以使用 shared_from_this() 在回调中安全持有对象的 shared_ptr,从而延长对象的生命周期,防止它在异步操作完成前被销毁。
注意事项
shared_from_this() 只能在对象已经通过 shared_ptr 管理时调用。如果在一个尚未通过 shared_ptr 管理的对象上调用 shared_from_this(),则会导致运行时错误(std::bad_weak_ptr 异常)。
- 当你希望某个对象能够通过多个地方持有它的
shared_ptr 时,继承 enable_shared_from_this 可以避免出现多个 shared_ptr 独立管理同一对象的情况。
五、weak_ptr
5.1 循环引用
循环引用:指两个或多个对象通过 shared_ptr 相互引用,导致它们的引用计数永远不会降为零,从而引发内存泄漏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class B;
class A { public: shared_ptr<B> ptrB; A() { std::cout << "A Constructor" << std::endl; }
~A() { std::cout << "A Destructor" << std::endl; } };
class B { public: shared_ptr<A> ptrA; B() { std::cout << "B Constructor" << std::endl; }
~B() { std::cout << "B Destructor" << std::endl; } };
int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>();
a->ptrB = b; b->ptrA = a;
return 0; }
|
使用weak_ptr解决循环引用问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class B;
class A { public: shared_ptr<B> ptrB; A() { std::cout << "A Constructor" << std::endl; }
~A() { std::cout << "A Destructor" << std::endl; } };
class B { public: weak_ptr<A> ptrA; B() { std::cout << "B Constructor" << std::endl; }
~B() { std::cout << "B Destructor" << std::endl; } };
int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>();
a->ptrB = b; b->ptrA = a;
return 0; }
|
5.2 相关成员函数
bool expired() const noexcept;
作用:检查 weak_ptr 是否已失效。如果对象已无效(对应的 shared_ptr 引用计数为 0),返回 true,否则返回 false。
shared_ptr<element_type> lock() const noexcept;
作用:尝试获取指向对象的 shared_ptr。如果对象还有效,则返回一个指向该对象的 shared_ptr;如果对象已无效或weak_ptr指向空,则返回空的 shared_ptr。
void reset() noexcept;
作用:将 weak_ptr 置为空。
long int use_count() const noexcept;
作用:返回与 weak_ptr 关联的 shared_ptr 的引用计数(即还有多少个 shared_ptr 引用该对象)。如果对象已失效或weak_ptr指向空,则返回 0。