C++ 智能指针

目录

为什么需要智能指针?

RAII

智能指针的原理

C++智能指针的历史

智能指针

auto_ptr

unique_ptr

shared_ptr

shared_ptr 引用计数解决智能指针拷贝问题

shared_ptr 循环引用问题

智能指针删除器

weak_ptr


为什么需要智能指针?

1. 我们在很多时候,使用new运算符在堆区申请空间,忘记释放则会带来内存泄漏,而智能指针可以自动释放堆区内存,自动执行delete。

2. 异常带来的内存泄漏隐患,可通过智能指针解决。

C++中某些函数或new运算符可能会抛异常,而如果我们在外层函数使用try catch捕获异常,则可能导致程序执行流跳转,即使new的后方有delete语句,也可能会因为捕获异常而不执行,从而造成内存泄漏,此时智能指针就是最好的解决方法。

int div()
{
    int a = 0, b = 0;
    cin >> a >> b;
    if(b == 0)
    {
        throw invalid_argument("除0错误");
    }
    return a / b;
}

void func1()
{
    // p1 p2 div三个地方抛异常会导致不同的内存泄漏
    int* p1 = new int(0);
    int* p2 = new int(1);

    cout << div() << endl;

    delete p1;
    delete p2;
    cout << "p1 and p2 were deleted" << endl;
}

void func2()
{
    unique_ptr<int> up1(new int(1));
    unique_ptr<int> up2(new int(2));

    cout << div() << endl;
}

int main()
{
    try
    {
        func1();
    }
    catch(const invalid_argument& ia)
    {
        cout << ia.what() << endl;
    }
    catch(...)
    {
        cout << "未知错误" << endl;
    }
//    test_auto_ptr();
//    test_smart_ptr2();
//    test_unique_ptr();
//    test_circular_reference();
//    test_weak_ptr();
    return 0;
}

如上,外层函数使用try catch语句,则在new之后发生异常,会导致delete语句被跳过,其中的new 和 div方法都可能抛异常,会造成不同程序的内存泄漏。

使用func2方法中的智能指针即可解决此问题,因为在异常抛出并捕获之后,尽管程序执行流会从div方法或func1(new)中跳转到main,但是函数栈帧仍会从内向外依次销毁,此时作为局部变量的up1 up2生命周期结束,调用析构函数,即可避免内存泄漏。
这就是智能指针使用的RAII思想。

RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。   

借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处: 不需要显式地释放资源。 采用这种方式,对象所需的资源在其生命期内始终保持有效。


智能指针的原理

1. RAII
有了上面的认识,其实RAII在智能指针这里就是,将堆区资源的创建和释放托管给一个对象,对象生命周期内,内存资源有效,对象生命周期结束时,自动调用析构函数,自动释放内存。

2. 像指针一样
类似于STL容器的迭代器,迭代器通过实现operator* operator->,将迭代器实现出指针的行为,使其使用起来像指针。
而智能指针,本身就是模拟指针,故智能指针类通过实现operator* operator->,使其具有指针一样的操作行为。

3. 智能指针的拷贝行为
我们知道, int* p = new int(1);    int* p2 = p; 此时p和p2指针内存储的地址相同,即指向同样的内存空间。所以,在实现智能指针时,必须考虑智能指针类的拷贝构造 和 operator=的行为。这里不能实现深拷贝,因为原生指针的拷贝本身就是浅拷贝,浅拷贝就是我们的目的。但是若两个智能指针指向相同,一个调用析构之后,另一个智能指针就是野指针。
所以,我们必须考虑如何解决智能指针的拷贝问题。

在C++标准库中,auto_ptr shared_ptr unique_ptr 都有着RAII和像指针一样的特性,但是它们对于指针的拷贝行为解决方法不同。具体看下方...

C++智能指针的历史

C++98中出现了auto_ptr,这是最原始的智能指针,它实现了RAII和像指针一样的行为,所以,可以说是解决了异常带来的内存安全隐患。但是auto_ptr对于智能指针的拷贝的实现非常糟糕...

C++98 ~ C++11之间,boost库实现了scoped_ptr shared_ptr weak_ptr。 在C++11,C++标准库新增了unique_ptr shared_ptr weak_ptr对应boost库的三大智能指针。这三个智能指针都有不同的特性。并且这些智能指针的实现原理是参考boost中的实现的。

智能指针

auto_ptr

    template <class T>
    class auto_ptr
    {
    public:
        explicit auto_ptr(T* p = nullptr)
        :_ptr(p)
        {}
        // auto_ptr拷贝构造的特点:资源转移
        auto_ptr(auto_ptr& ap)
        :_ptr(ap._ptr)
        {
            ap._ptr = nullptr;
        }
        auto_ptr<T>& operator=(auto_ptr<T>& ap)
        {
            if(this != &ap)
            {
                if(_ptr != nullptr)
                {
                    delete _ptr;
                }
                _ptr = ap._ptr;
                ap._ptr = nullptr;
            }
            return *this;
        }

        ~auto_ptr()
        {
            if(_ptr != nullptr)
                delete _ptr;
        }

        // 其实这里的T可能是const int / const Node
        // 明白吗?hhhhhh
        T& operator*() const
        {
           return *_ptr;
        }
        T* operator->() const
        {
            return _ptr;
        }
        T* get() const
        {
            return _ptr;
        }
    private:
        T* _ptr;
    };

auto_ptr的拷贝构造和operator=实现的是资源转移,也就是被拷贝的智能指针将被置为空。 
auto_ptr同样采用了RAII技术以及opeator* operator->

unique_ptr

删除器

    template <class T>
    struct default_delete
    {
        void operator()(T* p)
        {
            delete p;
        }
    };
    template <class T>
    struct delete_array
    {
        void operator()(T* p)
        {
            delete[] p;
        }
    };
    template <class T>
    struct Free
    {
        void operator()(T* p)
        {
            free(p);
        }
    };
    template <class T>
    struct Close
    {
        void operator()(T* p)
        {
            close(p);
        }
    };
    // 注意,如果是const int,则删除器就是default_delete<const int>
    template <class T, class D = default_delete<T>>
    class unique_ptr
    {
    public:
        explicit unique_ptr(T* p = nullptr)
        : _ptr(p)
        {}
        ~unique_ptr()
        {
            if(_ptr != nullptr)
            {
                D()(_ptr);
            }
        }
        // unique_ptr不允许拷贝,支持RAII和像指针一样
        unique_ptr(const unique_ptr<T>& up) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

        T& operator*() const
        {
            return *_ptr;
        }
        T* operator->() const
        {
            return _ptr;
        }
    private:
        T* _ptr;
    };

unique_ptr,如名,就是独一无二的智能指针,此智能指针同样采用了RAII和operator* operator->
不同的是对指针拷贝问题的解决:unique_ptr不支持拷贝构造和operator= 

shared_ptr

    template <class T, class D = yzl::default_delete<T>>
    class shared_ptr
    {
    public:
        explicit shared_ptr(T* p)
        :_ptr(p)
        , _pCount(new int(1))
        {}
        shared_ptr(const shared_ptr<T>& sp)
        :_ptr(sp._ptr)
        ,_pCount(sp._pCount)
        {
            // 有线程安全问题,之后学到了再解决。
            ++*_pCount;
        }
        shared_ptr& operator=(const shared_ptr<T>& sp)
        {
            // 防止自赋值
            if(_ptr != sp._ptr)
            {
                Release();
                _ptr = sp._ptr;
                _pCount = sp._pCount;
                ++*_pCount;
            }
            return *this;
        }
        ~shared_ptr()
        {
            Release();
        }

        T& operator*() const
        {
            return *_ptr;
        }
        T* operator->() const
        {
            return _ptr;
        }
        T* get() const
        {
            return _ptr;
        }
        int use_count() const
        {
            return *_pCount;
        }
    private:
        void Release()
        {
            if(--*_pCount == 0)
            {
                if(_ptr != nullptr)
                    D()(_ptr);
                delete _pCount;
            }
        }
    private:
        T* _ptr;
        int* _pCount;
    };

shared_ptr是功能最强大的智能指针,同样实现了RAII技术,operator*,operator->。

shared_ptr 引用计数解决智能指针拷贝问题

同时shared_ptr采用引用计数,实现了智能指针的拷贝。注意,这里的引用计数不能是int类型数据成员(很明显不可以),也不可以是静态int类型数据成员,因为这样的话所有的shared_ptr<int>智能指针使用的都是一个引用计数,不管它们的指向是否相同,这显然也是不行的。
解决方法就是一个int*类型数据成员,在shared_ptr的构造函数中,堆区开辟一个int,初始化为1。在每次拷贝构造 or operator=中++该引用计数。

shared_ptr 循环引用问题

    // 循环引用问题
    struct Node
    {
        int _val;

        std::shared_ptr<Node> _next;
        std::shared_ptr<Node> _prev;

//        std::weak_ptr<Node> _next;
//        std::weak_ptr<Node> _prev;

        ~Node()
        {
            cout << "~Node" << endl;
        }
    };

using yzl::Node;
// 循环引用
void test_circular_reference()
{
    shared_ptr<yzl::Node> n1(new yzl::Node);
    shared_ptr<Node> n2(new Node);

    cout << n1.use_count() << endl;
    cout << n2.use_count() << endl;

    // 解决循环引用:将内部的智能指针变为weak_ptr,而不是shared_ptr
    // 调用 weak_ptr<T>& operator=(const shared_ptr<T>& sp);
    n1->_next = n2;
    n2->_prev = n1;

//    n1->_next.lock()->_val = 10;
//    cout << n2->_val << endl;
    cout << n1.use_count() << endl;
    cout << n2.use_count() << endl;
};

 

形如上方场景,比如某类型(Node)中有shared_ptr类型数据成员,此时n1 和 n2本身也是两个智能指针。如上就会造成循环引用的问题:即当函数栈帧销毁,n1 n2 析构,此时引用计数--,因为_next _prev分别指向对方,引用计数减为1,不会释放资源。
此时左边Node想要释放,需要右边Node的_prev调用析构函数(引用计数减为0),而_prev作为Node的自定义类型数据成员,右边Node被析构时会自动调用_prev的析构函数,右边Node析构需要左边_next调用析构,左边_next调用析构需要左边Node释放。
此时的场景就会造成循环引用。

解决方法是,将Node的智能指针数据成员变为weak_ptr类型,weak_ptr的作用其实就是为了解决shared_ptr的循环引用问题。  解决方法是:weak_ptr不会增加引用计数,因此左右两边Node的引用计数分别为1,n1 n2调用析构,资源得以释放。

智能指针删除器

因为new/delete  new[]/delete[]   malloc/free  fopen/fclose(FILE*) 要配对使用,因此智能指针的析构函数不能固定实现为delete。

方式就是使用仿函数(函数对象),仿函数实现了operator()

    template <class T>
    struct default_delete
    {
        void operator()(T* p)
        {
            delete p;
        }
    };
    template <class T>
    struct delete_array
    {
        void operator()(T* p)
        {
            delete[] p;
        }
    };
    template <class T>
    struct Free
    {
        void operator()(T* p)
        {
            free(p);
        }
    };
    template <class T>
    struct Close
    {
        void operator()(T* p)
        {
            close(p);
        }
    };

注意:STL中unique_ptr的删除器是类模板参数,
如 unique_ptr<int, delete_array<int>> up(new int[3]);   即必须在类模板实参处传递仿函数类型
而std::shared_ptr的删除器实现在了构造函数的模板参数中,因此shared_ptr可以通过构造函数时传函数对象/lambda的方式传递删除器。
如 shared_ptr<int> sp(new int[3], [](int* p)->void{delete[] p; });

但是因为技术问题,仅能简单模拟shared_ptr的类模板形式的删除器

 

template<class T>
struct DeleteArray
{
    void operator()(T* p)
    {
        delete[] p;
    }
};
template<>
struct DeleteArray<string>
{
    void operator()(string* p)
    {
        cout << "Never Give up" << endl;
        delete[] p;
    }
};
template<class T>
struct Free
{
    void operator()(T* p)
    {
        free(p);
    }
};

struct Close
{
    void operator()(FILE* fp)
    {
        fclose(fp);
    }
};

void test()
{
    shared_ptr<int> sp(new int[3], [](int* p)->void{delete []p;});
    shared_ptr<string> sp2(new string[3], [](string* p){delete[] p;});
    shared_ptr<FILE> sp3(fopen("./../hehe.txt", "w+"), [](FILE* p)->void{fclose(p);});
    shared_ptr<int> sp4((int*)malloc(sizeof(int)*5), [](int* p){free(p);});
    unique_ptr<string, DeleteArray<string>> up4(new string[3]);
    unique_ptr<FILE, Close> up1(fopen("./../hhhh.txt", "w+"));
}

weak_ptr

weak_ptr就是为了解决shared_ptr的循环引用问题的。此智能指针没有RAII,没有operator* 和 operator->,不参与资源的创建和释放,不增加引用计数。

std::weak_ptr有一个lock成员函数,用于返回与此weak_ptr绑定的shared_ptr。可以借此访问资源。

weak_ptr没有以T*为参数的构造函数,仅能拷贝构造 or 以一个shared_ptr为参数的构造,所以说weak_ptr就是一个辅助型智能指针,不参与资源管理。 


内存泄漏... 略了

shared_ptr还有线程安全问题,略了,之后学了再说

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值