C++智能指针

·  阅读 592

Smart Pointer

C++没有垃圾回收机制,自己动态分配的内存需要自己释放,否则容易出现内存泄漏的问题,当你使用new了一块内存,在不使用的时候记得及时delete,但是,这其实是很难做到的,尤其当你的工程代码量比较大的时候,为了解决内存泄漏的问题,一个比较好的办法就是使用智能指针(smart pointer)。

智能指针存储指向动态分配(堆)对象指针的类,控制着该对象的生命周期,智能指针也是一个模板类,在定义的时候构造对象,在离开作用域的时候自动销毁分配的对象,内部实现了引用计数机制,每引用一次计数加一,每析构一次计数减一,当引用计数为0的时候释放所指向的堆内存。

C++11提供了3中智能指针,都包含在头文件中:

  • std::shared_ptr,允许多个对象指向同一个对象;
  • std::uniq_ptr,“独占”所指向的对象;
  • std::weak_ptr,是一种弱引用,指向std::shared_ptr所管理的对象;

1、std::shared_ptr

类似vector,在使用智能指针的时候需要给定它所指向的类型,指针的初始化可以通过构造函数、std::make_shared、reset()实现。

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> ptr1(new int(1));  //ptr1指向int类型
    std::shared_ptr<int> ptr2(ptr1);        //用ptr初始ptr2,此时ptr1指向的对象引用计数是2

    std::cout << "ptr1 引用计数:" << ptr1.use_count() << std::endl;

    std::shared_ptr<std::string> ptr3 = std::make_shared<std::string>("Hello World");                           //ptr3指向string类型

    std::cout << "ptr3 is : " << *ptr3 << std::endl;

    std::shared_ptr<std::string> ptr4;     //ptr4指向string类型
    ptr4.reset(new std::string("reset init"));

    std::cout << "ptr4 is : " << *ptr4<< std::endl;

    return 0;
}
复制代码
  • ptr1的初始化是通过构造函数实现;
  • ptr2的初始化是通过拷贝构造函数实现;
  • ptr3的初始化是通过std::make_shared实现;
  • ptr4的初始化是通过reset()实现,传入的是内置指针;

在查看ptr1所指向对象的引用计数调用了use_count()函数,当进行拷贝或赋值操作,std::shared_ptr都会记录有多少个其他的std::shared_ptr指向相同的对象,每一个std::shared_ptr调用该函数都能够获取所指向对象的指针个数。

//use_count函数定义 
_NODISCARD long use_count() const noexcept { // return use count
        return _Rep ? _Rep->_Use_count() : 0;
    }

//_Rep定义,指向_Ref_count_base类
 _Ref_count_base* _Rep{nullptr};

//Rep调用的_Use_count函数
long _Use_count() const noexcept { // return use count
        return static_cast<long>(_Uses);   //_Uses代表计数
    }
复制代码

在上面的几种初始化中,推荐使用std::make_shared方式,对于一个未初始化的智能指针,调用reset()可以将其初始化,对于一个已经初始化的智能指针,可以调用reset()释放其所指向对象。

//释放资源
void reset() noexcept { // release resource and convert to empty shared_ptr object
        shared_ptr().swap(*this);
    }

//指向_Ux类型的_Px对象
    template <class _Ux>
    void reset(_Ux* _Px) { // release, take ownership of _Px
        shared_ptr(_Px).swap(*this);
    }



#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> ptr1(new int(1));  //ptr1指向int类型
    std::shared_ptr<int> ptr2(ptr1); //用ptr初始ptr2,此时ptr1指向的对象引用计数是2

    std::cout << "ptr1 引用计数:" << ptr1.use_count() << std::endl;
    std::cout << "ptr2 引用计数:" << ptr2.use_count() << std::endl;

    ptr1.reset();    //释放ptr1指向的对象,此时只有ptr2指向该对象

    std::cout << "rest ptr1 引用计数:" << ptr1.use_count() << std::endl;
    std::cout << "ptr2 引用计数:" << ptr2.use_count() << std::endl;

    return 0;
}
复制代码

输出结果:

ptr1 引用计数:2
ptr2 引用计数:2
rest ptr1 引用计数:0
ptr2 引用计数:1
复制代码

在指针初始化过程中,不能利用内置指针给智能指针赋值,否则编译出错。

std::shared_ptr<int> ptr = new int(8); //无法从“int *”转换为“std::shared_ptr<int>”
复制代码

即使通过智能指针管理动态分配的对象资源,仍然可以获取其原始指针,调用其get()函数就可以实现。

_NODISCARD element_type* get() const noexcept { // return pointer to resource
        return _Ptr;
    }



#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> ptr(new int(8));
    int* p = ptr.get();
    std::cout << "*p is " << *p << std::endl;
    return 0;
}
复制代码

输出结果:

*p is 8
复制代码

对于自己实现的类对象,通过智能指针管理,可以让目标类继承 std::enable_shared_from_this ,通过shared_from_this()返回包含本类对象的this指针。

#include <iostream>
#include <memory>

class A : public std::enable_shared_from_this<A>{
public:
    void func()
    {
        std::cout << " A function" << std::endl;
    }

    std::shared_ptr<A> getSelf()
    {
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr<A> ptr(new A());
    std::shared_ptr<A> p = ptr->getSelf();

    p->func();

    return 0;
}
复制代码

使用std::shared_ptr的注意事项:

  1. 不要使用一个原始指针去初始化多个std::shared_ptr对象; int* p = new int(0); std::shared_ptr ptr1(p); std::shared_ptr ptr2(p); //避免这样

  2. 不要在参数中创建std::shared_ptr,可能出现内存泄漏 function(std::shared_ptr(new int), f());

    C++的传参顺序在不同的编译器下顺序可能不一样,如果从左向右顺序传参,先new int,然后调用f(),如果调用f()过程中出现了异常,而std::shared_ptr还没创建,则new int就出现了内存泄漏,所以,想要在参数中使用std::shared_ptr,应该提前创建好。

  3. 利用shared_from_this返回包含this指针的std::shared_ptr,不要单独将this返回出来。 #include #include

    class A : public std::enable_shared_from_this<A>{
    public:
        void func()
        {
            std::cout << " A function" << std::endl;
        }
    
        std::shared_ptr<A> getSelf()
        {
            return std::shared_ptr<A>(this);
        }
    };
    
    int main()
    {
        std::shared_ptr<A> ptr(new A());   //第一次利用this构造ptr
        std::shared_ptr<A> p = ptr->getSelf();//第二次利用this构造p
    
        p->func();
    
        return 0;
    }
    复制代码

    利用this指针两次构造std::shared_ptr,但是这两个std::shared_ptr之间是没有关系的,当这两个std::shared_ptr析构的时候就会对this指针析构两次。 //程序抛出异常的代码处 void _Decwref() noexcept { // decrement weak reference count if (_MT_DECR(_Weaks) == 0) { _Delete_this(); } }

  4. 避免循环引用 #include #include

    class B;
    class A {
    public:
        ~A()
        {
            std::cout << "A is des" << std::endl;
        }
        std::shared_ptr<B> aptr;
    };
    
    class B {
    public:
        ~B()
        {
            std::cout << "B is des" << std::endl;
        }
        std::shared_ptr<A> bptr;
    };
    
    int main()
    {
        std::shared_ptr<A> ptr1(new A());
        std::shared_ptr<B> ptr2(new B());
    
        ptr1->aptr = ptr2;  //循环引用
        ptr2->bptr = ptr1;
        
        std::cout << "A use count is " << ptr1.use_count() << std::endl;
        std::cout << "B use count is " << ptr2.use_count() << std::endl;
    
        return 0;
    }
    复制代码

    这里的类A与类B的析构函数都没有被调用,A与B的引用计数都是2,离开作用域变成1,永远不会析构,出现了内存泄漏问题。 A use count is 2 B use count is 2

2、std::unique_ptr

std::unique_ptr是独占型的智能指针,在某个时刻只能有一个std::unique_ptr指向一个给定对象,当std::unique_ptr被销毁的时候,它所指向的对象也被销毁,它不像std::shared_ptr共享所指向对象。

std::unique_ptr<int> ptr1(new int(0));
std::unique_ptr<int> ptr2 = ptr1;//错误,不能复制
std::unique_ptr<int> ptr3 = std::move(ptr1); //可以移动
复制代码

定义了ptr1指向int类型对象,由于std::unique_ptr是独占类型的指针,所以用ptr1给ptr2复制是错误的,但是它支持移动,可以将ptr1的所有权转移给ptr3,这样是可以的,移动后,ptr1不指向任何对象。

#include <iostream>
#include <memory>

int main()
{
    std::unique_ptr<std::string> ptr1(new std::string("unique_ptr"));
    std::cout << "ptr1 is " << *ptr1 << std::endl;

    std::unique_ptr<std::string> ptr2 = std::move(ptr1);
    std::cout << "ptr2 is " << *ptr2 << std::endl;

    return 0;
}
复制代码

输出结果:

ptr1 is unique_ptr
ptr2 is unique_ptr
复制代码

在C++11中,没有类似std::make_shared的初始化方法,但是在C++14中,对于std::unique_ptr引入了std::make_unique方法进行初始化。

#include <iostream>
#include <memory>

int main()
{
    std::unique_ptr<std::string> ptr1(new std::string("unique_ptr"));
    std::cout << "ptr1 is " << *ptr1 << std::endl;

    std::unique_ptr<std::string> ptr2 = std::make_unique<std::string>("make_unique init!");
    std::cout << "ptr2 is " << *ptr2 << std::endl;

    return 0;
}
复制代码

输出结果:

ptr1 is unique_ptr
ptr2 is make_unique init!
复制代码

3、std::weak_ptr

std::weak_ptr是弱引用指针,一般主要是配合std::shared_ptr使用,它不会改变所指向对象引用计数的值,也不管理std::shared_ptr内部指针,一般只监视std::shared_ptr的生命周期,std::weak_ptr没有重载*与->符号,因为它不共享指针,也不能操作资源,看似好像没有存在的价值,其实不然,std::weak_ptr可以用来返回this指针和解决循环引用问题。

std::shared_ptr<int> ptr1(new int(8));
std::weak_ptr<int> wptr(ptr1); //wptr弱共享ptr1
复制代码

这里wptr弱共享ptr1,对于ptr1所指向的对象,我们不能通过wptr去访问,因为wptr只能通过expired()函数判断其若共享的对象是否存在,如果弱引用的对象存在,std::weak_ptr中的lock()函数用来获取一个指向共享对象的std::shared_ptr。

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> sptr1(new int(8));
    std::shared_ptr<int> sptr2(sptr1);
    std::weak_ptr<int> uptr1(sptr1);

    std::cout << "weak ptr use count is " << uptr1.use_count() << std::endl;
    if (uptr1.expired())    //判断资源是否被释放
    {
        std::cout << "uptr1 is expired" << std::endl;
    }
    else
    {
        auto p = uptr1.lock();  //如果没有释放,则获取std::shared_ptr
        std::cout << "uptr1 is " << *p << std::endl;
    } 

    sptr1.reset();  //将资源释放掉
    sptr2.reset();

    if (uptr1.expired())  //此时资源已经释放
    {
        std::cout << "uptr1 is expired" << std::endl;
    }
    else
    {
        auto p = uptr1.lock();
        std::cout << "uptr1 is " << *p << std::endl;
    }

    return 0;
}
复制代码
  • 定义sptr1、sptr2指向同一个对象;
  • 定义uptr1弱共享sptr1、sptr2指向的对象;
  • 通过uptr1获取其弱共享对象的引用计数,uptr1.use_count();
  • 调用uptr1的expired()函数判断资源是否释放,如果没有释放,就通过lock()获取对应的std::shared_ptr;
  • 释放资源,再次调用uptr1的expired函数判断资源是否释放,此时弱共享的对象已经释放;

输出结果:

weak ptr use count is 2
uptr1 is 8
uptr1 is expired
复制代码

expired()函数的定义:

//可以看出expired函数就是在判断其引用计数是否为0
    _NODISCARD bool expired() const noexcept { // return true if resource no longer exists
        return this->use_count() == 0;
    }
复制代码

lock()函数的定义:

//lock函数返回对应std::shared_ptr<T>
_NODISCARD shared_ptr<_Ty> lock() const noexcept { // convert to shared_ptr
        shared_ptr<_Ty> _Ret;
        (void) _Ret._Construct_from_weak(*this);
        return _Ret;
    }
复制代码

在std::shared_ptr中,可以通过shared_from_this获取包含this指针的std::shared_ptr,这是因为enable_shared_from_this类中存在一个std::weak_ptr。

  _NODISCARD shared_ptr<_Ty> shared_from_this() { // return shared_ptr
        return shared_ptr<_Ty>(_Wptr);
    }
复制代码

std::weak_ptr还能解决循环引用问题,减少内存泄漏情况的发生。

#include <iostream>
#include <memory>

class B;
class A {
public:
    ~A()
    {
        std::cout << "A is des" << std::endl;
    }
    std::shared_ptr<B> aptr;
};

class B {
public:
    ~B()
    {
        std::cout << "B is des" << std::endl;
    }
    std::weak_ptr<A> bptr;            //将bptr定义为指向A类型对象的弱引用
};

int main()
{
    std::shared_ptr<A> ptr1(new A());  //ptr1指向A类型对象
    std::shared_ptr<B> ptr2(new B());  //ptr2指向B类型对象

    ptr1->aptr = ptr2;   // aptr指向B对象,此时两个std::shared_ptr指向B类型对象
    ptr2->bptr = ptr1;   //bptr是弱引用,此时还是只有一个std::shared_ptr指向A类型对象

    std::cout << "A use count is " << ptr1.use_count() << std::endl;
    std::cout << "B use count is " << ptr2.use_count() << std::endl;

    return 0;
}
复制代码

输出结果:

A use count is 1
B use count is 2
A is des
B is des
复制代码

由于B中的bptr是A的弱引用,所以A的引用计数是1,当A退出作用域的时候就析构了,当A析构后,其成员aptr也析构了,这样只剩下ptr2指向B了,此时引用计数为1,当ptr2离开作用域后,B就被析构了,此时不会出现内存泄漏。

总结,在实际的项目开发中,根据场景选择合适的智能指针,尽量避免使用new与delete。

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改