C++ 智能指针

std::shared_ptr<T>

定义 

        std::shared_ptr<T>是一个类模板,它的对象行为像指针,但是它还能记录有多少个对象共享它管理的内存对象。多个std::shared_ptr<T>可以共享同一个对象。当最后一个std::shared_ptr<T>被销毁时,它会自动释放它所指向的对象。一个shared_ptr<T>指针可以通过make_shared<T>函数来创建,也可以通过拷贝或赋值另一个shared_ptr来创建。

std::shared_ptr的底层原理        

element_type*	   _M_ptr;         // Contained pointer.
__shared_count<_Lp>  _M_refcount;    // Reference counter.

        std::shared_ptr在内部维护两个指针成员,一个指针是所管理的数据的地址;还有一个指针是控制块的地址,包括引用计数、weak_ptr计数、删除器(Deleter)、分配器(Allocator)。因为不同shared_ptr指针需要共享相同的内存对象,因此引用计数的存储是在堆上的。而unique_ptr只有一个指针成员,指向所管理的数据的地址。因此一个shared_ptr对象的大小是raw_pointer大小的两倍。

// 32位编译器下
std::cout<<sizeof(std::shared_ptr<int>)<<std::endl; // 8
std::cout<<sizeof(std::unique_ptr<int>)<<std::endl; // 4

std::shared_ptr<T>的内置方法

/ shared_ptr constructor example
#include <iostream>
#include <memory>

struct C {int* data;};

int main () {
  std::shared_ptr<int> p1;
  std::shared_ptr<int> p2 (nullptr);
  std::shared_ptr<int> p3 (new int);
  std::shared_ptr<int> p4 (new int, std::default_delete<int>());
  std::shared_ptr<int> p5 (new int, [](int* p){delete p;}, std::allocator<int>());
  std::shared_ptr<int> p6 (p5);
  std::shared_ptr<int> p7 (std::move(p6));
  std::shared_ptr<int> p8 (std::unique_ptr<int>(new int));
  std::shared_ptr<C> obj (new C);
  std::shared_ptr<int> p9 (obj, obj->data);

  std::cout << "use_count:\n";
  std::cout << "p1: " << p1.use_count() << '\n';
  std::cout << "p2: " << p2.use_count() << '\n';
  std::cout << "p3: " << p3.use_count() << '\n';
  std::cout << "p4: " << p4.use_count() << '\n';
  std::cout << "p5: " << p5.use_count() << '\n';
  std::cout << "p6: " << p6.use_count() << '\n';
  std::cout << "p7: " << p7.use_count() << '\n';
  std::cout << "p8: " << p8.use_count() << '\n';
  std::cout << "p9: " << p9.use_count() << '\n';
  return 0;
}

尽量使用std::make_shared<T>而不是shared_ptr<T>(new T)

std::make_shared的构造过程

假设有如下程序:

#include <iostream>
#include <memory>
using namespace std;
class A{
    int a;
    A(int input):a(input){}
};
int main(){
    std::shared_ptr<A> a = std::make_shared<A>(0);
}

那么在对象a的构造过程中会依次调用如下函数:

首先std::make_shared会将其实现委托给std::allocate_shared。

//call stack #4
template<typename _Tp, typename... _Args>   
    inline shared_ptr<_Tp>
    make_shared(_Args&&... __args)
    {
        typedef typename std::remove_cv<_Tp>::type _Tp_nc;
        return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
                std::forward<_Args>(__args)...);
    }

std::allocate_shared与std::make_shared功能基本相同,唯一的不同是可以为std::allocate_shared指定allocator,比如在std::make_shared中就为std::allocate_shared指定了默认的std::allocator.

// call stack #3
template<typename _Tp, typename _Alloc, typename... _Args>  
    inline shared_ptr<_Tp>
    allocate_shared(const _Alloc& __a, _Args&&... __args)
    {
        return shared_ptr<_Tp>(_Sp_alloc_shared_tag<_Alloc>{__a},
                std::forward<_Args>(__args)...);
    }

然后在std::allocate_shared中,调用了shared_ptr的构造函数,此时为了在shared_ptr的诸多构造函数中选择适用于allocate_shared的构造函数,使用了tag dispatch技术(_Sp_alloc_shared_tag)。

// call stack #2
template<typename _Alloc, typename... _Args> 
        shared_ptr(_Sp_alloc_shared_tag<_Alloc> __tag, _Args&&... __args)
        : __shared_ptr<_Tp>(__tag, std::forward<_Args>(__args)...)
        { }

通过tag dispatch技术选出了上面的shared_ptr构造函数,该构造函数继续通过tag dispatch技术调用了特定的__shared_ptr:

// call stack #1         
template<typename _Alloc, typename... _Args>   
                __shared_ptr(_Sp_alloc_shared_tag<_Alloc> __tag, _Args&&... __args)
                : _M_ptr(), _M_refcount(_M_ptr, __tag, std::forward<_Args>(__args)...)
                { _M_enable_shared_from_this_with(_M_ptr); }

在这个函数中首先对指向数据存储区内存的指针_M_ptr进行了默认初始化,然后会在_M_refcount的构造中对_M_ptr进行赋值。在选择适用于make_shared的_M_refcount的构造函数时,再次使用了tag dispatch。

// call stack #0        
template<typename _Tp, typename _Alloc, typename... _Args>   
            __shared_count(_Tp*& __p, _Sp_alloc_shared_tag<_Alloc> __a,
                    _Args&&... __args)
            {
                typedef _Sp_counted_ptr_inplace<_Tp, _Alloc, _Lp> _Sp_cp_type;
                typename _Sp_cp_type::__allocator_type __a2(__a._M_a);
                auto __guard = std::__allocate_guarded(__a2);
                _Sp_cp_type* __mem = __guard.get();
                auto __pi = ::new (__mem)
                    _Sp_cp_type(__a._M_a, std::forward<_Args>(__args)...);
                __guard = nullptr;
                _M_pi = __pi;
                __p = __pi->_M_ptr();
            }

在大概知晓了std::make_shared的构造过程之后,下面来解释为什么它是“最为精妙,最为高效,也最广为推荐的”shared_ptr构造方式。

常规的std::shared_ptr构造

通常data field和control_block是分离的,比如当我们通过如下代码创建一个shared_ptr时:

std::shared_ptr<int> a(new int);

首先,我们为int类型new出了一块内存,然后在构造__shared_count的时候,又会为control block new出一块内内存,第二次内存分配代码如下:

template<typename _Ptr>
            explicit
            __shared_count(_Ptr __p) : _M_pi(0)
            {
                __try
                {
                    _M_pi = new _Sp_counted_ptr<_Ptr, _Lp>(__p);
                }
                __catch(...)
                {
                    delete __p;
                    __throw_exception_again;
                }
            }
            ...
            _Sp_counted_base<_Lp>*  _M_pi;

为了构建一个std::shared_ptr对象,却进行了两次内存分配,而且第二次内存分配分配的内存还比较小,这一方面会影响程序性能,另一方面还会大大增加内存碎片产生的可能性。

使用std::make_shared的std::shared_ptr构造

std::make_shared的精妙之处就在于,它将std::shared_ptr构造中的两次内存分配降低到了一次。这会对提供程序性能和降低内存碎片都有帮助。

其具体实现过程需要参考// call stack #0 中的代码和后文中_Sp_counted_ptr_inplace的相关代码。

在//call stack #0中的__shared_ptr里,会通过萃取技术为_Sp_counted_ptr_inplace开辟出一块内存,其中_Sp_counted_ptr_inplace唯一的数据成员是_Impl类型的_M_impl。下面来看_Sp_counted_ptr_inplace中直接以及间接包含的信息:

  1. 由于_Sp_counted_ptr_inplace的父类是_Sp_counted_base,而_Sp_counted_base里有_M_use_count和_M_weak_count两个成员,因此_Sp_counted_ptr_inplace间接的包含了_M_use_count和_M_weak_count。
  2. 由于_M_impl继承自 _Sp_ebo_helper<0, _Alloc>,而 _Sp_ebo_helper<0, _Alloc>是一个为了使用空基类优化(EBCO)而引入的辅助类,因此_Sp_counted_ptr_inplace还间接的包含了一个allocator对象。
  3. 由于_M_impl还有一个__gnu_cxx::aligned_buffer<_Tp> _M_storage成员,而__gnu_cxx::aligned_buffer<_Tp>包含的是一个大小和经过内存对其后的_Tp的大小相同的char数组,其目的是用来存储_Tp,因此_Sp_counted_ptr_inplace还间接包含了一个_Tp。

上述1和2对应于control block,3对应于data fiels。因此在//call stack #0中,通过类型萃取std::__allocate_guarded为_Sp_counted_ptr_inplace开辟内存,就等于同时为data field和control block开辟了内存。这也正是std::make_shared的精妙所在。

shared_ptr的线程安全问题

如果多个线程同时拷贝同一个 shared_ptr 对象,不会有问题,因为 shared_ptr 的引用计数是线程安全的。但是如果多个线程同时修改同一个 shared_ptr 对象,不是线程安全的。因此,如果多个线程同时访问同一个 shared_ptr 对象,并且有写操作,需要使用互斥量来保护。

  • 引用计数更新,线程安全(原子操作)

这里我们讨论对shared_ptr进行拷贝的情况,由于此操作读写的是引用计数,而引用计数的更新是原子操作,因此这种情况是线程安全的。下面这个例子,两个线程同时对同一个shared_ptr进行拷贝,引用计数的值总是20001。

std::shared_ptr<int> p = std::make_shared<int>(0);
constexpr int N = 10000;
std::vector<std::shared_ptr<int>> sp_arr1(N);
std::vector<std::shared_ptr<int>> sp_arr2(N);

void increment_count(std::vector<std::shared_ptr<int>>& sp_arr) {
    for (int i = 0; i < N; i++) {
        sp_arr[i] = p;
    }
}

std::thread t1(increment_count, std::ref(sp_arr1));
std::thread t2(increment_count, std::ref(sp_arr2));
t1.join();
t2.join();
std::cout<< p.use_count() << std::endl; // always 20001
  • 同时修改内存区域,线程不安全

下面这个例子,两个线程同时对同一个shared_ptr指向内存的值进行自增操作,最终的结果不是我们期望的20000。因此同时修改shared_ptr指向的内存区域不是线程安全的。


std::shared_ptr<int> p = std::make_shared<int>(0);
void modify_memory() {
    for (int i = 0; i < 10000; i++) {
        (*p)++;
    }
}

std::thread t1(modify_memory);
std::thread t2(modify_memory);
t1.join();
t2.join();
std::cout << "Final value of p: " << *p << std::endl; // possible result: 16171, not 20000

自定义删除器 Custom Deleter

自定义删除器的使用场景

自定义删除器的作用是在智能指针释放所管理的对象时,执行一些特殊的操作,比如:

  • 内存释放时打印一些日志。
  • 管理除内存以外的其它资源,例如文件句柄、数据库连接等。
  • 与自定义分配器(Allocator)配合使用,将资源释放给自定义分配器。

自定义删除器的使用

自定义删除器可以是一个函数,也可以是一个类的对象, 也可以是一个lambda表达式。

如果是一个函数,它的形式如下:

void free_memory(int* p) {
    std::cout << "delete memory" << std::endl;
    delete p;
}

如果是一个类的对象,它的形式如下:

class FreeMemory {
public:
    void operator()(int* p) {
        std::cout << "delete memory" << std::endl;
        delete p;
    }
};

如果是一个lambda表达式,它的形式如下:

auto free_memory_lambda = [](int* p) {
    std::cout << "delete memory" << std::endl;
    delete p;
}
  • shared_ptr自定义删除器的使用:

    对于shared_ptr, 不管删除器什么类型,是否有状态都不会增加shared_ptr的大小, 均为两个字长。因为删除器是存储在控制块中,而控制块的大小为两个字长。

unique_ptr自定义删除器的使用

  • unique_ptr的删除器类型是一个模板参数,因此需要指定删除器类型。
  • 如果删除器是函数指针类型,std::unique_ptr大小从1个字长增长到2个字长,因为需要存储函数指针。
  • 如果删除器是无状态删除器(stateless function),比如不进行捕获的lambda表达式,std::unique_ptr大小不变,因为无状态删除器不需要存储任何成员变量。
std::unique_ptr<int, FreeMemory> up1(new int(0)); // size: 4
std::unique_ptr<int, void(*)(int*)> up2(new int(0), free_memory);  // size: 8
std::unique_ptr<int, decltype(free_memory)*> up3(new int(0), free_memory); // size: 4

有状态删除器和无状态删除器 

什么是有状态删除器?什么是无状态删除器?有状态删除器是指删除器类中包含有成员变量,无状态删除器是指删除器类中不包含有成员变量。

如果std::unique_ptr的函数对象删除器是具有扩展状态的,其大小可能会非常大。如果大得无法接受,可能需要设计一个无状态删除器。

下面是一个有状态删除器的例子:

class DeleteObject {
public:
    DeleteObject(int n) : n_(n) {}
    void operator()(int* p) {
        std::cout << "delete memory " << n_ << std::endl;
        delete p;
    }
private:
    int n_;
};

避免用同一个raw pointer初始化多个shared_ptr 

为什么不要用同一个raw pointer初始化多个shared_ptr

因为多个shared_ptr由同一个raw pointer创建时会导致生成两个独立的引用计数控制块,从以下程序可见sp1、sp2的引用计数都为1。

int* p = new int(0);
std::shared_ptr<int> sp1(p);
std::shared_ptr<int> sp2(p);
std::cout<<sp1.use_count()<<std::endl; // 1
std::cout<<sp2.use_count()<<std::endl; // 1

当sp1、sp2销毁时会产生未定义行为,因为shared_ptr的析构函数会释放它所管理的对象,当sp1析构时,会释放p指向的内存,当sp2析构时,会再次释放p指向的内存。

weak_ptr

底层原理

std::weak_ptr是一种智能指针,它指向一个std::shared_ptr管理的对象。但是,它不会增加对象的引用计数,因此,它不会影响对象的生命周期。这种指针的主要作用是协助std::shared_ptr工作,它可以访问std::shared_ptr管理的对象,但是它不拥有该对象。std::weak_ptr可以从std::shared_ptr或另一个std::weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少。

std::shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp(sp);
std::cout << "wp.use_count() = " << wp.use_count() << std::endl; // 

如何使用std::weak_ptr 

如何读取引用对象?

weak_ptr对它所指向的shared_ptr所管理的对象没有所有权,不能对它解引用,因此若要读取引用对象,必须要转换成shared_ptr。 C++中提供了lock函数来实现该功能。如果对象存在,lock()函数返回一个指向共享对象的shared_ptr,否则返回一个空shared_ptr

 如何判断weak_ptr指向对象是否存在呢?

weak_ptr提供了一个成员函数expired()来判断所指对象是否已经被释放。如果所指对象已经被释放,expired()返回true,否则返回false。

程序示例:

std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;
std::weak_ptr<int> wp = sp1; // point to sp1
std::cout<<wp.use_count()<<std::endl; // 2
if(!wp.expired()){
    std::shared_ptr<int> sp3 = wp.lock();
    std::cout<<*sp3<<std::endl; // 22
}

std::weak_ptr构造std::shared_ptr

std::weak_ptr可以作为std::shared_ptr的构造函数参数,但如果std::weak_ptr指向的对象已经被释放,那么std::shared_ptr的构造函数会抛出std::bad_weak_ptr异常。

std::shared_ptr<int> sp1(new int(22));
std::weak_ptr<int> wp = sp1; // point to sp1
std::shared_ptr<int> sp2(wp);
std::cout<<sp2.use_count()<<std::endl; // 2
sp1.reset();
std::shared_ptr<int> sp3(wp); // throw std::bad_weak_ptr

std::weak_ptr 一些内置方法

方法用途
use_count()返回与之共享对象的shared_ptr的数量
expired()检查所指对象是否已经被释放
lock()返回一个指向共享对象的shared_ptr,若对象不存在则返回空shared_ptr
owner_before()提供所有者基于的弱指针的排序
reset()释放所指对象
swap()交换两个weak_ptr对象

应用场景 

用于实现缓存

weak_ptr可以用来缓存对象,当对象被销毁时,weak_ptr也会自动失效,不会造成野指针。

假设我们有一个Widget类,我们需要从文件中加载Widget对象,但是Widget对象的加载是比较耗时的。

std::shared_ptr<Widget> loadWidgetFromFile(int id); 
// a factory function which returns a shared_ptr, which is expensive to call
// may perform file or database I/O

因此,我们希望Widget对象可以缓存起来,当下次需要Widget对象时,可以直接从缓存中获取,而不需要重新加载。这个时候,我们就可以使用std::weak_ptr来缓存Widget对象,实现快速访问。如以下代码所示:


std::shared_ptr<Widget> fastLoadWidget(int id) {
    static std::unordered_map<int, std::weak_ptr<Widget>> cache; // long lifetime
    auto objPtr = cache[id].lock(); 
    if (!objPtr) {
        objPtr = loadWidgetFromFile(id);
        cache[id] = objPtr; // use std::shared_ptr to construct std::weak_ptr
    }
    return objPtr;
}

当对应id的Widget对象已经被缓存时,cache[id].lock()会返回一个指向Widget对象的std::shared_ptr,否则cache[id].lock()会返回一个空的std::shared_ptr,此时,我们就需要重新加载Widget对象,并将其缓存起来,这一步会由std::shared_ptr构造std::weak_ptr

为什么不直接存储std::shared_ptr呢?静态的std::unordered_map具有长生命周期,其中存储的std::shared_ptr会导致缓存中的对象永远不会被销毁,因为std::shared_ptr的引用计数永远不会为0。而std::weak_ptr不会增加对象的引用计数,因此,当缓存中的对象没有被其他地方引用时,std::weak_ptr会自动失效,从而导致缓存中的对象被销毁。

避免循环引用

std::weak_ptr可以用来解决使用std::shared_ptr时可能导致的循环引用问题。

  • 什么是“循环引用” ?

循环引用是指两个或多个对象之间通过shared_ptr相互引用,形成了一个环,导致它们的引用计数都不为0,从而导致内存泄漏。

在观察者模式中使用shared_ptr可能会出现循环引用,在下面的程序中,Observer对象和Subject对象相互引用,导致它们的引用计数都无法减到0,从而可能导致内存泄漏。

class IObserver {
public:
    virtual void update(const string& msg) = 0;
};

class Subject {
public:
    void attach(const std::shared_ptr<IObserver>& observer) {
        observers_.emplace_back(observer);
    }
    void detach(const std::shared_ptr<IObserver>& observer) {
        observers_.erase(std::remove(observers_.begin(), observers_.end(), observer), observers_.end());
    }
    void notify(const string& msg) {
        for (auto& observer : observers_) {
            observer->update(msg);
        }
    }
private:
    std::vector<std::shared_ptr<IObserver>> observers_;
};

class ConcreteObserver : public IObserver {
public:
    ConcreteObserver(const std::shared_ptr<Subject>& subject) : subject_(subject) {}
    void update(const string& msg) override {
        std::cout << "ConcreteObserver " << msg<< std::endl;
    }
private:
    std::shared_ptr<Subject> subject_;
};

int main() {
    std::shared_ptr<Subject> subject = std::make_shared<Subject>();
    std::shared_ptr<IObserver> observer = std::make_shared<ConcreteObserver>(subject);
    subject->attach(observer);
    subject->notify("update");
    return 0;
}
  • std::weak_ptr避免循环引用的方法

将Observer类中的subject_成员变量改为weak_ptr,这样就不会导致内存无法正确释放了。

 用于实现单例模式

单例模式是指一个类只能有一个实例,且该类能自行创建这个实例的一种模式。单例模式的实现方式有很多种,其中一种就是使用std::weak_ptr

class Singleton {
public:
    static std::shared_ptr<Singleton> getInstance() {
        std::shared_ptr<Singleton> instance = m_instance.lock();
        if (!instance) {
            instance.reset(new Singleton());
            m_instance = instance;
        }
        return instance;
    }
private:
    Singleton() {}
    static std::weak_ptr<Singleton> m_instance;
};

std::weak_ptr<Singleton> Singleton::m_instance;

std::weak_ptr实现单例模式的优点:

  1. 避免循环应用:避免了内存泄漏。
  2. 访问控制:可以访问对象,但是不会延长对象的生命周期。
  3. 可以在单例对象不被使用时,自动释放对象。

用于实现enable_shared_from_this模板类

std::enable_shared_from_this是一个模板类,它的作用是为了解决在类成员函数中获取std::shared_ptr的问题。它提供了一个成员函数shared_from_this(),该函数返回一个指向当前对象的std::shared_ptr

  • 作用:用于在类对象的内部中获得一个指向当前对象的 shared_ptr 对象

  • 解决问题: 如果通过this指针创建shared_ptr时,相当于通过一个裸指针创建shared_ptr,多次创建会导致多个shared_ptr对象管理同一个内存。当shared_ptr对象销毁时,会释放this指向的内存,但是this指针可能还会被使用,导致程序崩溃。

class A {
public:
    std::shared_ptr<A> get_shared_ptr() {
        return std::shared_ptr<A>(this); // error
    }
};

  • 使用方法: 继承enable_shared_from_this类;通过shared_from_this()方法返回。
class A : public std::enable_shared_from_this<A> {
public:
    std::shared_ptr<A> get_shared_ptr() {
        return shared_from_this();
    }
};
  • 原理:在类中维护一个weak_ptr,将weak_ptr作为参数传入shared_ptr的构造函数,返回一个shared_ptr对象。
template<typename _Tp>
class enable_shared_from_this
{
protected:
    constexpr enable_shared_from_this() noexcept = default;
    enable_shared_from_this(const enable_shared_from_this&) noexcept = default;
    enable_shared_from_this& operator=(const enable_shared_from_this&) noexcept = default;
    ~enable_shared_from_this() = default;
public:
    shared_ptr<_Tp> shared_from_this()
    {
        shared_ptr<_Tp> __p(_M_weak_this);
        return __p;
    }

    shared_ptr<const _Tp> shared_from_this() const
    {
        shared_ptr<const _Tp> __p(_M_weak_this);
        return __p;
    }

    weak_ptr<_Tp> weak_from_this() noexcept // C++17
    {
        return _M_weak_this;
    }

    weak_ptr<const _Tp> weak_from_this() const noexcept // C++17
    {
        return _M_weak_this;
    }

    template<typename _Up> friend class shared_ptr;
};
  • 限制:只能用于继承自enable_shared_from_this的类。

  • 适用场景:在类的内部需要获得一个指向当前对象的shared_ptr对象时,可以使用enable_shared_from_this模板类。

  • 36
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

**K

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值