C++智能指针

C++提供了四个智能指针模版类,分别为:auto_ptr,unique_ptr,shared_ptrweak_ptr(其中auto_ptr为C++98提供的解决方案,在C++11中已废除,并提供另外三种)。这三者均定义了类似指针的对象,可以将new获得(直接或间接)的地址赋给这种对象。当智能指针过期时,其析构函数将使用delete来释放内存。(创建智能指针对象需要头文件<memory>)

三种指针的区别?

  1. std::unique_ptr<T>:独占资源所有权的指针。当进行赋值时,会将旧指针的所有权转让,使得对于特定对象,只能有一个智能指针可以拥有它。相比于auto_ptr会执行更严格的所有权转让策略。
  2. std::shared_ptr<T>:共享资源所有权的指针。通过引用计数(reference counting),跟踪引用特定对象的智能指针数。当发生赋值操作时,计数增1,当指针过期时,计数减1.仅当最后一个指针过期时,才调用delete
  3. std::weak_ptr<T>:共享资源的观察者,需要与shared_ptr一起使用,不影响资源的生命周期:它指向一个shared_ptr管理的对象,而进行内存管理的只有shared_ptr.weak_ptr主要用来帮助解决循环引用问题,它的构造与析构函数不会引起引用计数的增加或减少。
  4. std::auto_ptr:当进行赋值时,会将旧指针的所有权转让,使得对于特定对象,只能有一个智能指针可以拥有它。——已被废除

unique_ptr和auto_ptr的区别?

  • 所有权转让机制不同:auto_ptr允许通过直接赋值进行转让,但是这样会留下危险的悬挂指针,容易使得程序在运行阶段崩溃。unique_ptr仅仅允许将临时右值进行赋值,否则会在编译阶段发生错误,更加安全。
  • 相较于auto_ptr和shared_ptr,unique_ptr可以使用new[]分配的内存作为参数:std::unique_ptr<double[]> pda(new double(5));

使用三种指针的例子

std::unique_ptr(基于RAII思想)

  1. 使用裸指针时,要记得释放内存:
    int* p = new int(100);
//……
    delete p;//释放内存

而使用std::unique_ptr自动管理内存:

    std::unique_ptr<int> uptr = std::make_unique<int>(100);
    //……
    //离开uptr作用域时自动释放内存
  1. std::unique_ptr是move_only的:
    std::unique_ptr<int> uptr = std::make_unique<int>(100);
    std::unique_ptr<int> uptr1 = uptr;//编译错误
        //unique_ptr只有移动构造函数,因此只能移动(转移内部对象所有权:浅拷贝),不能拷贝(深拷贝)
    
    std::unique_ptr<int> uptr2 = std::move(uptr);
        //unique_ptr只有移动赋值重载函数,参数是&&,只能接右值,因此必须用std::move转换类型
    assert(uptr == nullptr);
  1. std::unique_ptr可以指向一个数组:
    std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);
    for(int i = 0; i < 10; i++){
        uptr[i] = i;
    }
    for(int i = 0; i < 10; i++){
        std::cout << uptr[i] << std::endl;
    }
  1. 自定义deleter:
struct FileCloser{
    void operator()(FILE* fp) const{
        if(fp != nullptr){
            fclose(fp);
        }
    }
};
std::unique_ptr<FILE, FileCloser> uptr(fopen(“file.txt”, “w”));

使用Lambda的deleter:

    std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(
        fopen(“file.txt”, “w”), [](FILE* fp){
            fclose(fp);
        });

这种写法存在一个问题就是因为std::function本来就不是lambda的原类型,std::function是通用多态函数封装器,非常强大但有代价,需要一定的内存开销。而把std::function作为类型传给了unique_ptr deleter时,等于在unique_ptr里把这个std::function也给存起来,这时开销就大了……


改进的写法就是用decltype直接获取lambda原类型,同样可以进行EBO(空基类优化:如果deleter是个空基类并且可以继承的话,就不需要保存这个deleter类型的成员,直接继承这个deleter类型):

    std::unique_ptr<FILE, decltype(FileCloser)> uptr1(fopen(“file.txt”, “w”), FileCloser);
    //C++17不允许uptr1只有第一个参数,也就是必须带着第二个参数FileCloser即使FileCloser没存进unique_ptr中。
    //C++20允许了std::unique_ptr<FILE, decltype(FileCloser)> uptr1(fopen(“file.txt”, “w”));

std::shared_ptr

  1. shared_ptr对资源进行引用计数并且当引用计数为0时自动释放资源:
    std::shared_ptr<int> sptr = std::make_shared<int>(500);
    assert(sptr.use_count() == 1);//创建完毕后引用计数为1
    {
        std::shared_ptr<int> sptr1 = sptr;
        assert(sptr.get() == sptr1.get());
        assert(sptr.use_count() == 2);//sptr与sptr1共享资源
    }
    assert(sptr.use_count() == 1);//sptr1已经释放
//use_count()为0时自动释放内存
  1. 与unique_ptr相同,shared_ptr也可以指向数组和自定义deleter:
{
    //std::shared_ptr<int[]> sptr = std::make_shared<int[]>(100);
    //C++20才支持std::make_shared<int[]>
    std::shared_ptr<int[]> sptr(new int[10]);
    for(int i = 0; i < 10; i++){
        sptr[i] = i;
    }
    for(int i = 0; i < 10; i++){
        std::cout << sptr[i] << std::endl;
    }
}

{
    std::shared_ptr<FILE> sptr(
        fopen(“file.txt”, “w”), [](FILE* fp){
            std::cout << “close ” << fp << std::endl;
            fclose(fp);
        });
}

std::weak_ptr

std::weak_ptr要与std::shared_ptr一起使用。一个weak_ptr对象看错是shared_ptr对象管理管理的资源的观察者,它不影响共享资源的生命周期:

  1. 如果需要使用weak_ptr正在观察的资源,可以将weak_ptr提升为shared_ptr。
  2. 当shared_ptr管理的资源释放时,weak_ptr会自动变为nullptr。
void Observe(std::weak_ptr<int> wptr){
    if(auto sptr = wptr.lock()){
        std::cout << “value: ” << *sptr << std::endl;
    }else{
        std::cout << “wptr lock fail” << std::endl;
    }
}

std::weak_ptr<int> wptr;
{
    auto sptr = std::make_shared<int>(521);
    wptr = sptr;
    Observe(wptr);//sptr指向资源未被释放,wptr可成功提升为shared_ptr
}
Observe(wptr);//sptr指向资源已被释放,无法提升

  • 当shared_ptr析构并释放共享资源的时候,只要weak_ptr对象还存在,控制块就会保留,weak_ptr可以通过控制块观察对象是否存活。

std::shared_ptr实现原理

shared_ptr的开销

一个shared_ptr对象内存开销比裸指针和无自定义deleter的unique_ptr对象略大:

    using namespace std;
    cout << sizeof(int*) << endl;//8
    cout << sizeof(unique_ptr<int>) << endl;//8
    cout << sizeof(unique_ptr<FILE, function<void(FILE*)>>)
         << endl;//40 ! ! !
         
    cout << sizeof(shared_ptr<int>) << endl;//16
    shared_ptr<FILE> sptr(fopen("test_file.txt", "w"), [](FILE* fp) {
    cout << "close " << fp << endl;
    fclose(fp);
  }); 
    cout << sizeof(sptr) << endl;  //16
  • 其中无自定义deleter的unique_ptr只是把裸指针用RAII手法封装,无需保存其他信息,故开销与裸指针相同。
  • 而shared_ptr需要维护两部分信息:
  1. 指向共享资源的指针。
  2. 引用计数等共享资源的控制信息(维护一个指向控制信息的指针)
  • 另外,在使用shared_ptr时,会涉及两次内存分配:一次分配共享资源对象;一次分配控制块。
    C++标准库提供std::make_shared函数来创建一个shared_ptr对象,只需一次内存分配。

shared_ptr支持aliasing constructor(别名构造函数)

template <class U>
shared_ptr(const shared_ptr<U>& x, element_type* p) noexcept;

此时构造出的shared_ptr对象并不拥有p,也不会管理p的内存;而是和x共同拥有x管理的对象,并增加x的一个计数,同时负责x指向对象的内存管理。简单来说,aliasing构造方式,构造了一个拥有x(负责x生命管理周期),但是指向p(访问p的数据)的共享指针。


有两个概念,一个是stored pointer 存储指针(访问该对象的数据),一个是owned pointer 所有者指针(负责该对象的生存周期管理);
实际应用的举例如下:

  struct C{int * data};
  std::shared_ptr<C> obj (new C); //obj是C类型对象的一个共享指针
  std::shared_ptr<int> p9 (obj, obj->data); //p9是obj的一个共享指针,但指向的是C对象的data数据成员
  cout << *p9 << endl; //访问的是obj->data
  cout << p9->get() << endl; //访问的是obj->data
这里,访问p9存储的数据时,实际上是访问的obj->data,也就是p9是obj->data的一个stored pointer,不负责obj->data的生存周期管理;

而p9实际上管理的是obj的生存周期,也就是p9是obj的owned pointer;

aliasing constructor这种用法实际上是为了解决一种场景:一个智能指针有可能指向了另一个智能指针中的某一部分,但又要保证这两个智能指针销毁时,只对那个被指的对象完整地析构一次,而不是两个指针分别析构一次。

shared_ptr::owner_before()

  • 在标准库的shared_ptr中,operator<,比较的是stored pointer,因此上面举例的那种情况,p9和obj两个shared_ptr是不相等的;而owner_before()是基于owner pointer的比较,因此p9和obj是相等的;
  • shared_ptr作为map的key时,用的就是owner_before()而不是operator<,否则可能不满足我们实际的使用需求;
  • 注意,boost库中的shared_ptr和标准库中的share_ptr实现有所不同;boost库中operator<和owner_before()都是比较的owner pointer;

enable_shared_from_this

一个类的成员函数如何获得指向自身(this)的shared_ptr?

class Foo {
 public:
  std::shared_ptr<Foo> GetSPtr() {
    return std::shared_ptr<Foo>(this);
  }
};

auto sptr1 = std::make_shared<Foo>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 1);
assert(sptr2.use_count() == 1);

这里的代码会生成两个独立的shared_ptr,它们的控制块是独立的,最终导致一个Foo对象被析构两次。

成员函数获取this的shared_ptr正确做法为继承std::enable_shared_from_this:

class Bar : public std::enable_shared_from_this<Bar> {
 public:
  std::shared_ptr<Bar> GetSPtr() {
    return shared_from_this();
  }
};

auto sptr1 = std::make_shared<Bar>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 2);
assert(sptr2.use_count() == 2);

一般情况下,继承了 std::enable_shared_from_this 的子类,成员变量中增加了一个指向 this 的 weak_ptr。这个 weak_ptr 在第一次创建 shared_ptr 的时候会被初始化,指向 this。

总结

智能指针,本质上是对资源所有权和生命周期管理的抽象:

  1. 当资源是被独占时,使用 std::unique_ptr 对资源进行管理。
  2. 当资源会被共享时,使用 std::shared_ptr 对资源进行管理。
  3. 使用 std::weak_ptr 作为 std::shared_ptr 管理对象的观察者。
  4. 通过继承 std::enable_shared_from_this 来获取 this 的 std::shared_ptr 对象。

参考资料

posted @ 2022-10-12 09:30  yytarget  阅读(155)  评论(0编辑  收藏  举报