走进C++11(三十三)shared_ptr

关注公众号获取更多信息:

 

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

 

1. shared_ptr是什么

 

首先shared_ptr是一个模板类。它是一种重载了->和*操作符的类,由类来实现对内存的管理,确保即使有异常产生,也可以通过智能指针类的析构函数完成内存的释放。

 

2. 为什么要用shared_ptr

 

shared_ptr并不能使程序变得更快,相反,由于控制块的引入,会使程序占用更多的内存。相应的引用计数逻辑会增加程序的运行开销。那么为什么要使用shred_ptr呢?

 

share_ptr,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。

 

每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候, 内存才会被释放。确保不会多次释放或者悬空指针。

 

归根到底一句话就是:尽量减少内存的错误使用。

 

如果你写程序靠谱,不会出现内存泄漏,重复删除指针,悬空指针等情况,建议不要使用shared_ptr。

 

3. shared_ptr是如何实现它的特性的

 

可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。

 

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数析构函数(destructor)来完成销毁工作的。类似于构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。

 

4. shared_ptr 如何创建

 

可以通过构造函数、赋值函数或者make_shared函数初始化智能指针。

 

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。当要用make_shared时,必须指定想要创建的对象的类型,定义方式与模板类相同。在函数名之后跟一个尖括号,在其中给出类型。例如,调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配。如果不传递任何参数,对象就会进行值初始化。

 

Note:1. shared_ptr的默认能力是管理动态内存,但支持自定义的Deleter以实现个性化的资源释放动作。

2. 如果我们不初始化一个智能指针,它就会被初始化成一个空指针,接受指针参数的职能指针是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须直接初始化形式来初始化一个智能指针

 

shared_ptr<int> p1 = new int(1024);//错误:必须使用直接初始化形式shared_ptr<int> p2(new int(1024));//正确:使用了直接初始化形式

 

使用shared_ptr注意事项:

 

不要把一个原生指针给多个shared_ptr管理
不要把this指针给shared_ptr
不要在函数实参里创建shared_ptr
不要在函数实参里创建shared_ptr
通常用auto定义一个对象来保存make_shared的结果
不要不加思考地把指针替换为shared_ptr来防止内存泄漏,shared_ptr并不是万能的,而且使用它们的话也是需要一定的开销的
环状的链式结构shared_ptr将会导致内存泄漏(可以结合weak_ptr来解决)
共享拥有权的对象一般比限定作用域的对象生存更久,从而将导致更高的平均资源使用时间
在多线程环境中使用共享指针的代价非常大,这是因为你需要避免关于引用计数的数据竞争
共享对象的析构器不会在预期的时间执行
不使用相同的内置指针值初始化(或reset)多个智能指针
不delete get()返回的指针
不使用get()初始化或reset另一个智能指针
如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
shared_ptr的类型转换不能使用一般的static_cast,这种方式进行的转换会导致转换后的指针无法再被shared_ptr对象正确的管理。应该使用专门用于shared_ptr类型转换的 static_pointer_cast<T>() , const_pointer_cast<T>() 和dynamic_pointer_cast<T>()

 

下面我们看一下官网对shared_ptr详细的描述:

 

定义于头文件 <memory>

 

template< class T > class shared_ptr;

(C++11 起)

 

std::shared_ptr是通过指针保持对象共享所有权的智能指针。多个 shared_ptr 对象可占有同一对象。下列情况之一出现时销毁对象并解分配其内存:

 

  • 最后剩下的占有对象的 shared_ptr 被销毁;

  • 最后剩下的占有对象的 shared_ptr 被通过 operator= 或 reset() 赋值为另一指针。

     

用 delete 表达式或在构造期间提供给 shared_ptr 的定制删除器销毁对象。

shared_ptr 能在存储指向一个对象的指针时共享另一对象的所有权。此特性能用于在占有其所属对象时,指向成员对象。存储的指针为 get() 、解引用及比较运算符所访问。被管理指针是在 use_count 抵达零时传递给删除器者。

shared_ptr 亦可不占有对象,该情况下称它为空 (empty) (空 shared_ptr 可拥有非空存储指针,若以别名使用构造函数创建它)。

 

shared_ptr 的所有特化满足可复制构造 (CopyConstructible) 、可复制赋值 (CopyAssignable) 和可小于比较 (LessThanComparable) 的要求并可按语境转换为 bool 。

 

多个线程能在 shared_ptr 的不同实例上调用所有成员函数(包含复制构造函数与复制赋值)而不附加同步,即使这些实例是副本,且共享同一对象的所有权。若多个执行线程访问同一 shared_ptr 而不同步,且任一线程使用 shared_ptr 的非 const 成员函数,则将出现数据竞争;原子函数的 shared_ptr 特化能用于避免数据竞争。

 

成员类型

 

 
成员类型定义
element_type
T(C++17 前)
std::remove_extent_t<T>(C++17 起)
weak_type (C++17 起)std::weak_ptr<T>

 

成员函数

 

(构造函数)

构造新的 shared_ptr (公开成员函数)

(析构函数)

如果没有更多 shared_ptr 指向持有的对象,则析构对象 (公开成员函数)

operator=

对 shared_ptr 赋值 (公开成员函数)

修改器

reset

替换所管理的对象  (公开成员函数)

swap

交换所管理的对象  (公开成员函数)

观察器

get

返回存储的指针 (公开成员函数)

operator*operator->

解引用存储的指针 (公开成员函数)

operator[](C++17)

提供到被存储数组的带下标访问 (公开成员函数)

use_count

返回 shared_ptr 所指对象的引用计数 (公开成员函数)

unique(C++20 前)

检查所管理对象是否仅由当前 shared_ptr 的实例管理 (公开成员函数)

operator bool

检查是否有关联的管理对象  (公开成员函数)

owner_before

提供基于拥有者的共享指针排序  (公开成员函数)

非成员函数

make_sharedmake_shared_for_overwrite(C++20)

创建管理一个新对象的共享指针
(函数模板)

allocate_sharedallocate_shared_for_overwrite(C++20)

创建管理一个用分配器分配的新对象的共享指针
(函数模板)

static_pointer_castdynamic_pointer_castconst_pointer_castreinterpret_pointer_cast(C++17)

应用 static_cast、dynamic_cast、const_cast 或 reinterpret_cast 到被存储指针
(函数模板)

get_deleter

返回指定类型中的删除器,若其拥有  (函数模板)

operator==operator!=operator<operator<=operator>operator>=operator<=>

(C++20 中移除)(C++20 中移除)(C++20 中移除)(C++20 中移除)(C++20 中移除)(C++20)

与另一个 shared_ptr 或 nullptr 进行比较  (函数模板)

operator<<

将存储的指针的值输出到输出流
(函数模板)

std::swap(std::shared_ptr)(C++11)

特化 std::swap 算法  
(函数模板)

std::atomic_is_lock_free(std::shared_ptr)std::atomic_load(std::shared_ptr)std::atomic_load_explicit(std::shared_ptr)std::atomic_store(std::shared_ptr)std::atomic_store_explicit(std::shared_ptr)std::atomic_exchange(std::shared_ptr)std::atomic_exchange_explicit(std::shared_ptr)std::atomic_compare_exchange_weak(std::shared_ptr)std::atomic_compare_exchange_strong(std::shared_ptr)std::atomic_compare_exchange_weak_explicit(std::shared_ptr)std::atomic_compare_exchange_strong_explicit(std::shared_ptr)

(C++20 中弃用)

特化的原子操作  
(函数模板)

辅助类

std::hash<std::shared_ptr>

(C++11)

std::shared_ptr 的散列支持
(类模板特化)

std::atomic<std::shared_ptr>(C++20)

原子共享指针 (类模板特化)

 

注意

 

只能通过复制构造或复制赋值其值给另一 shared_ptr ,将对象所有权与另一 shared_ptr 共享。用另一 shared_ptr 所占有的底层指针创建新的 shared_ptr 导致未定义行为。

 

std::shared_ptr 可以用于不完整类型 T 。然而,参数为裸指针的构造函数( template<class Y> shared_ptr(Y*) )和 template<class Y> void reset(Y*) 成员函数只可以用指向完整类型的指针调用(注意 std::unique_ptr 可以从指向不完整类型的裸指针构造)。

 

实现说明

 

在典型的实现中, std::shared_ptr 只保有二个指针:

  • get() 所返回的指针

  • 指向控制块的指针

控制块是一个动态分配的对象,其中包含:

  • 指向被管理对象的指针或被管理对象本身

  • 删除器(类型擦除)

  • 分配器(类型擦除)

  • 占有被管理对象的 shared_ptr 的数量

  • 涉及被管理对象的 weak_ptr 的数量

以调用 std::make_shared 或 std::allocate_shared 创建 shared_ptr 时,以单次分配创建控制块和被管理对象。被管理对象在控制块的数据成员中原位构造。通过 shared_ptr 构造函数之一创建 shared_ptr 时,被管理对象和控制块必须分离分配。此情况中,控制块存储指向被管理对象的指针。

shared_ptr 持有的指针是通过 get() 返回的;而控制块所持有的指针/对象则是最终引用计数归零时会被删除的那个。两者并不一定相等。

shared_ptr 的析构函数会将控制块中的 shared_ptr 计数器减一,如果减至零,控制块就会调用被管理对象的析构函数。但控制块本身直到 std::weak_ptr 计数器同样归零时才会释放。

既存实现中,若有共享指针指向同一控制块,则自增弱指针计数 ([1], [2]) 。

为满足线程安全要求,引用计数器典型地用等价于用 std::memory_order_relaxed 的 std::atomic::fetch_add 自增(自减要求更强的顺序,以安全销毁控制块)。

 

举个小例子:

 

#include <iostream>#include <memory>#include <thread>#include <chrono>#include <mutex> struct Base{    Base() { std::cout << "  Base::Base()\n"; }    // 注意:此处非虚析构函数 OK    ~Base() { std::cout << "  Base::~Base()\n"; }}; struct Derived: public Base{    Derived() { std::cout << "  Derived::Derived()\n"; }    ~Derived() { std::cout << "  Derived::~Derived()\n"; }}; void thr(std::shared_ptr<Base> p){    std::this_thread::sleep_for(std::chrono::seconds(1));    std::shared_ptr<Base> lp = p; // 线程安全,虽然自增共享的 use_count    {        static std::mutex io_mutex;        std::lock_guard<std::mutex> lk(io_mutex);        std::cout << "local pointer in a thread:\n"                  << "  lp.get() = " << lp.get()                  << ", lp.use_count() = " << lp.use_count() << '\n';    }} int main(){    std::shared_ptr<Base> p = std::make_shared<Derived>();     std::cout << "Created a shared Derived (as a pointer to Base)\n"              << "  p.get() = " << p.get()              << ", p.use_count() = " << p.use_count() << '\n';    std::thread t1(thr, p), t2(thr, p), t3(thr, p);    p.reset(); // 从 main 释放所有权    std::cout << "Shared ownership between 3 threads and released\n"              << "ownership from main:\n"              << "  p.get() = " << p.get()              << ", p.use_count() = " << p.use_count() << '\n';    t1.join(); t2.join(); t3.join();    std::cout << "All threads completed, the last one deleted Derived\n";}

 

可能的输出:

 

Base::Base()  Derived::Derived()Created a shared Derived (as a pointer to Base)  p.get() = 0x2299b30, p.use_count() = 1Shared ownership between 3 threads and releasedownership from main:  p.get() = 0, p.use_count() = 0local pointer in a thread:  lp.get() = 0x2299b30, lp.use_count() = 5local pointer in a thread:  lp.get() = 0x2299b30, lp.use_count() = 3local pointer in a thread:  lp.get() = 0x2299b30, lp.use_count() = 2  Derived::~Derived()  Base::~Base()All threads completed, the last one deleted Derived
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值