C++智能指针简介



  • C++智能指针简介

    一.使用背景

    由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete,比如流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见,并造成内存泄露。因此C++引用了智能指针,智能指针即是C++ RAII的一种应用,可用于动态资源管理,资源即对象的管理策略。

    智能指针在<memory>标头文件的 std 命名空间中定义。

    智能指针的优点

    • 1)智能指针能够帮助我们处理资源泄露问题;

    • 2)它也能够帮我们处理空悬指针的问题;

    • 3)它还能够帮我们处理比较隐晦的由异常造成的资源泄露。

    二.各类智能指针用法简介

    C++ 智能指针主要包括:unique_ptr,shared_ptr, weak_ptr, 这三种(auto_ptr 已被遗弃)

    1.shared_ptr的使用

    1)shared_ptr多个指针指向相同的对象

    shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存,每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。

    2)创建方式

    shared_ptr的创建,有两种方式

    shared_ptr<int> p1 = make_shared<int>(1);// 通过make_shared函数
    shared_ptr<int> p2(new int(2));// 通过原生指针构造
    

    3)注意事项

    • 智能指针是一个类不是指针,不能将指针直接赋值给一个智能指针
      • 例如std::shared_ptr<int> p4 = new int(1);的写法是错误的
    • 拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,后来指向的对象引用计数加1
    • 不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
    • 循环引用问题

    4)代码示例

    #include <iostream>
    #include <memory>
    
    int main() {
        {
            int a = 1;
            std::shared_ptr<int> ptra = std::make_shared<int>(a);
            std::shared_ptr<int> ptra2(ptra); //拷贝,引用计数+1
            std::cout << ptra.use_count() << std::endl; //2
    
            int b = 2;
            int *pb = &a;
            //std::shared_ptr<int> ptrb = pb;  //将指针赋值给一个智能指针 error
            std::shared_ptr<int> ptrb = std::make_shared<int>(b);
            ptra2 = ptrb; //赋值,引用计数-1
            pb = ptrb.get(); //获取原始指针
    
            std::cout << ptra.use_count() << std::endl; //2
            std::cout << ptrb.use_count() << std::endl; //1
        }
        //超出作用域,内存释放
    }
    

    2.unique_ptr的使用

    1)unique_ptr同一时刻只能有一个unique_ptr指向给定对象

    unique_ptr禁止拷贝语义、只能通过移动语义转移所有权

    2)创建方式

    unique_ptr的创建,与shared_ptr相似,也是两种方式

    shared_ptr<int> p1 = make_unique<int>(1);
    std::unique_ptr<int> uptr(new int(2));
    

    3)注意事项

    • 通过reset方法重新指定
    • 通过移动语义转移所有权
    • 通过release方法释放所有权

    4)代码示例

    #include <iostream>
    #include <memory>
    
    int main() {
        {
            std::unique_ptr<int> uptr(new int(10));  //绑定动态对象
            //std::unique_ptr<int> uptr2 = uptr;  //不能赋值 error
            //std::unique_ptr<int> uptr2(uptr);  //不能拷贝 error
            std::unique_ptr<int> uptr2 = std::move(uptr); //转移所有权
            uptr2.reset(new int(20));  //重新指定对象
            uptr2.release(); //释放所有权
        }
        //超出uptr的作用域,内存释放
    }
    

    2.weak_ptr的使用

    1)weak_ptr是为了配合shared_ptr而引入的一种智能指针

    weak_ptr的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况

    2)创建方式

    weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权

    std::weak_ptr<int> wp1(sh_ptr); //通过shared_ptr构造
    std::weak_ptr<int> wp2(wp1);/   //通过另一个weak_ptr构造
    

    3)注意事项

    • weak_ptr可以使用成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象
    • weak_ptr的成员函数expired()用来观测资源的引用计数是否为0,若use_count()==0,则expired()==true

    4)代码示例

    #include <iostream>
    #include <memory>
    
    int main() {
        {
            std::shared_ptr<int> sh_ptr = std::make_shared<int>(1);
            std::cout << sh_ptr.use_count() << std::endl;//1
    
            std::weak_ptr<int> wp(sh_ptr);
            std::cout << wp.use_count() << std::endl; //1,构造weak_ptr不增加引用计数
    
            if(!wp.expired()){
                std::shared_ptr<int> sh_ptr2 = wp.lock(); //使用成员函数lock()
                *sh_ptr2 = 100;
                std::cout << wp.use_count() << std::endl; //2
            }
        }
        //超出作用域,内存释放
    }
    

    三.循环引用问题

    1.循环引用代码示例

    class B;
    class A
    {
    public:
      shared_ptr<B> m_b;
    };
     
    class B
    {
    public:
      shared_ptr<A> m_a;
    };
     
    int main()
    {
        {
        shared_ptr<A> a(new A); //new出来的A的引用计数此时为1
        shared_ptr<B> b(new B); //new出来的B的引用计数此时为1
        a->m_b = b; //B的引用计数增加为2
        b->m_a = a; //A的引用计数增加为2
      }
     
      //b先出作用域,B的引用计数减少为1,不为0,所以堆上的B空间没有被释放,且B持有的A也没有机会被析构,A的引用计数也完全没减少
     
      //a后出作用域,同理A的引用计数减少为1,不为0,所以堆上A的空间也没有被释放
     
        //a和b互相抓住对方的引用不放,导致内存泄漏
    }
    

    2.解除循环引用的方法

    一般来讲,解除这种循环引用有下面有三种可行的方法(参考):

    • 1)当只剩下最后一个引用的时候需要手动打破循环引用释放对象
    • 2)当A的生存期超过B的生存期的时候,B改为使用一个普通指针指向A
    • 3)使用弱引用的智能指针打破这种循环引用

    但方法1和方法2都需要程序员手动控制,麻烦且容易出错,所以我们一般使用第三种方法:弱引用的智能指针weak_ptr

    对于示例中的代码,只需要将shared_ptr<B> m_b改为weak_ptr<B> m_b,则B的引用计数不会增加为2,B出作用域后可以正常释放,A也就能正常释放了

    三.shared_ptr智能指针类的简单实现

    1.智能指针的原理

    • 1)创建新对象时,初始化指针,并设置引用计数为1
    • 2)当对象作为另外一个对象的副本创建,也就是调用拷贝构造函数时,拷贝指针,并且,增加引用计数
    • 3)当对一个对象进行赋值时,左操作数所指对象的引用计数减少,如果减少为0,则删除对象,右操作数所指对象的引用计数增加
    • 4)调用析构函数,减少引用计数,如果减至0,则删除指针

    2.智能指针的实现

    引用计数的实现由两种经典策略:引入辅助类和使用句柄。

    1)辅助类实现引用计数

    • U_Ptr作为辅助类,封装实际的指针对象和引用计数值
    • HasPtr作为对外使用的类,构造时传入实际的指针对象
    • HasPtr内部包含了一个辅助类U_Ptr的指针对象,多个HasPtr类对象指向同一个U_Ptr对象
    • U_Ptr依靠引用计数来实现实际指针对象的释放
    //模板类作为友元类,要事先声明
    template <class T> 
    class HasPtr;
     
    //辅助类
    template<typename T>
    class U_Ptr {
    	friend class HasPtr<T>;	//友元类
    	T *ip;	//实际指针对象
    	size_t use;	//引用计数
    	U_Ptr(T *p)
                :ip(p), use(1)
        { }//构造函数
    	~U_Ptr() //析构函数
    	{
    		delete ip;
    	}
    };
     
    template<typename T>
    class HasPtr 
    {
    public:
    	// 构造函数,引用计数初始化为1
    	explicit HasPtr(T *p)
    		:ptr(new U_Ptr<T>(p))
    	{ }
     
    	// 拷贝构造函数,引用计数加1
    	HasPtr(const HasPtr<T> &orig)
    		:ptr(orig.ptr)
    	{
    		++ptr->use;
    	}
     
    	//赋值
    	HasPtr<T>& operator=(const HasPtr<T>& rhs)
    	{
    		++rhs.ptr->use;     // 操作符右值自加
    		if (--ptr->use == 0)	//左值自减,并判断是否减至0
    			delete ptr;    // 减至0则删除
    		ptr = rhs.ptr;      // 拷贝指针
    		return *this;
     
    	}
     
    	// 析构,自减
    	~HasPtr()
    	{ 
    		if (--ptr->use == 0)
    			delete ptr;
    	}
     
        //重载箭头运算符
    	T* operator->()
    	{
    		if(ptr)
    			return ptr;
    		throw std::runtime_error("access through NULL pointer");
    	}
     
    	const T* operator->() const
    	{
    		if(ptr)
    			return ptr;
    		throw std::runtime_error("access through NULL pointer");
    	}
     
        //重载解引用运算符
    	T& operator*()
    	{
    		if(ptr)
    			return *ptr;
    		throw std::runtime_error("dereference of NULL pointer");
    	}
     
    	const T& operator*() const
    	{
    		if(ptr)
    			return *ptr;
    		throw std::runtime_error("dereference of NULL pointer");
    	}
     
    private:
    	U_Ptr<T> *ptr;        // 辅助类对象
    };
    

    2)句柄类实现引用计数

    不用引入辅助类,可以直接把指针封装起来。然后,重载操作符,定义为一个指针的行为

    • 1)定义一个SmartPtr对象P1,传入实际指针对象,调用构造函数,初始化计数为1
    • 2)定义一个SmartPtr对象P2,调用拷贝构造函数,此时,P1和P2的ptr指向相同的地址,pUse指向相同的地址,引用计数自加
    • 3)定义一个SmartPtr对象P3,调用赋值,操作符右操作数引用计数自加,左操作数自减,并判断原引用计数是否为0,如果是0,则删除原ptr指针指向的地址内容,赋值ptr和pUse,指向相同的ptr和pUse
    • 4)析构时,引用计数自减,并判断计数值是否为0,如果是0,则自动删除指针对象
    template<typename T>
    class SmartPtr
    {
    public:
    	SmartPtr(T* p= 0)  //构造函数
    		:ptr(p),pUse(new size_t(1))
    	{}
     
    	~SmartPtr() //析构函数
    	{
    		decrUse();
    	}
     
        //拷贝构造函数,引用计数加1
    	SmartPtr(const SmartPtr<T>& src)
    		:ptr(src.ptr),pUse(src.pUse)
    	{
    		++*pUse;
    	}
     
        //赋值
    	SmartPtr<T>& operator=(const SmartPtr<T>& rhs)
    	{
    		if (rhs.ptr != ptr)
    		{
    			++*rhs.pUse;
    			decrUse();
    			ptr = rhs.ptr;
    			pUse = rhs.pUse;
    		}
    		return *this;
    	}
    
        //重载箭头运算符
    	T* operator->()
    	{
    		if(ptr)
    			return ptr;
    		throw std::runtime_error("access through NULL pointer");
    	}
     
    	const T* operator->() const
    	{
    		if(ptr)
    			return ptr;
    		throw std::runtime_error("access through NULL pointer");
    	}
     
        //重载解引用运算符
    	T& operator*()
    	{
    		if(ptr)
    			return *ptr;
    		throw std::runtime_error("dereference of NULL pointer");
    	}
     
    	const T& operator*() const
    	{
    		if(ptr)
    			return *ptr;
    		throw std::runtime_error("dereference of NULL pointer");
    	}
     
    private:
     
    	void decrUse()
    	{
    		if(--*pUse == 0)
    		{
    			delete ptr;
    			delete pUse;
    		}
    	}
     
    private:
    	T* ptr;
    	size_t* pUse;
    };
    


  • 其实还有auto_ptr,但基本不使用, 大多数时候被unique_ptr代替了



  • @ruisongzhou c++11中auto_ptr已经被舍弃了


 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待