C++智能指针

1.什么是只能指针?为什么要有智能指针?

#include 

int main()
{
    int *p1 = new int(); 
    //其他操作...
    return 0;
}

        以上面这段代码为例,如果我们向内存中new了一块空间,等同于C语言中的malloc函数,内存给我们返回了一块空间的地址,我们用指针保存,在进行了一系列操作后,并没有进行释放,而是直接退出程序,那么这样会产生什么问题?

        存在内存泄漏的问题,我们都知道如果内存泄漏问题并不是小问题,很多高级程序设计语言都加了很多“保险”来预防内存问题的出现,因为一个计算机的内存空间是有限的,如果我们只用不释放,相当于这块内存始终被我们使用,当进程中出现许多执行流,对这段代码进行频繁的调用,因为一块的错误会导致整个计算机性能大大下降,所以这个问题是我们一定要避免的。

        内存泄漏问题并不是失去了这块内存,并不是指物理层面的消息,而是应用程序分配某段内存后,因为设计错误,失去了对某段内存的控制权,从而导致了内存的浪费。

        所以,我们在使用new关键字开辟对象后,一定要养成使用delete关键字对其进行释放,对应C语言的关键字是malloc/free。

2.内存泄露的分类

        C/C++程序中一般我们只关心两种方面的泄漏:

        ①堆内存泄漏:

                堆内存指的是程序执行中依据须要分配通过malloc/calloc/realloc/new等从堆中分配一块

        内存,用完后必须通过调用响应的free和delete删掉。假设程序设计错误导致这部分内存没有

被释放,那么以后这部分空间将无法再使用,就会产生堆内存泄漏(Heap Leak)。

        ②系统资源泄漏:

                指程序使用系统分配的资源,比如套接字,文件描述符,管道等没有使用对应的函数释放,导致系统资源的浪费,严重可导致系统效能减少,系统执行严重不稳定。

3.RAII

        RAII(Resource Acquisition IInitialization) - 资源获取即初始化。这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入;

        为了避免程序员在写完代码后忘记释放资源这个习惯,C++引入这项技术,运用了C++语言局部对象出了作用域自动销毁的特性来控制资源的生命周期。

Class Person
{
public:
    Person(const string& name = "", int age = 0)
        :_name(name)
        ,_age(age)
    {
        std::cout << "Person()" << std::endl;
    }
    const std::string& getname() const 
    {
        return this->_name;
    }
    int getage()
    {
        return this->_age;
    }
    ~Person()
    {
        std::cout << "~Person()" << std::endl;
    }
private:
    const std::string _name;
    int _age;
}
int main()
{
    Person p;
    reutrn 0;
}

        在运行这段代码后,结果为:Person();~Person();所以我们能看到Person对象是先创建了p对象,然后我们在没有手动释放时自动调用了Person的析构函数进行了删除。

        也可以使用指针来实现:

template
class SmartPtr
{
public:
    SmartPtr(T* ptr = nullptr)
        :_ptr(ptr)
    {}
    ~SmartPtr()
    {
        if(_ptr)
        {
            delete _ptr;
        }
    }
private:
    T* _ptr;
}

        上述代码还不足以成为智能指针,作为指针它必须要有指针的功能,而作为指针,最重要的功能就是解引用去访问空间中的内容,所以我们还需要重载* 和->两个操作符。

T& operator*() {return *this->_ptr};
T* operator->() {return this->_ptr};

4.std::auto_ptrC++智能指针_第1张图片

namespace zq
{
	template
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		auto_ptr(auto_ptr& sp)
			:_ptr(sp._ptr)
		{
			//管理权转移
			sp._ptr = nullptr;
		}

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
        auto_ptr& operator=(auto_ptr& ap)
        {
            if(this != &ap)
            {
                if(this->_ptr)
                {
                    delete _ptr;
                }
                _ptr = ap._ptr;
                ap._ptr = nullptr;
            }
            return *this;
        }
		//像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return ptr;
		}

	private:
		T* _ptr;
	};
}

        我们可以看到,auto_ptr在使用时使用了管理权转移的方法,在使用auto_ptr管理资源时,auto_ptr会先将传进来的new出来的对象,然后auto_ptr会用传进来的地址赋值给自己的指针,然后再释放掉传进来的指针所指向的空间,将资源转移给了auto_ptr管理,也就是说ap对象现在是空的,但是如果下文中使用ap对象去访问了其成员,就会出现错误,针对这个特性,auto_ptr也是被建议尽量不要使用的。

2.std::unique_ptrC++智能指针_第2张图片

        unique_ptr的设计思路非常的粗暴 - 防拷贝,正如其名,“唯一的指针”,也就是不让拷贝和赋值。

	template>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		unique_ptr(const unique_ptr& sp) = delete;
		unique_ptr& operator=(const unique_ptr& sp) = delete;

		~unique_ptr()
		{
			if (_ptr)
			{
				//cout << "delete:" << _ptr << endl;
				//delete _ptr;

				D del;
				del(_ptr);
			}
		}
		//像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;

		}

	private:
		T* _ptr;
	};

        我们可以看到,在unique_ptr的模拟实现中,拷贝赋值和赋值运算符的重载都被delete掉,

private:
    UniquePtr(UniquePtr const &);
    UniquePtr & operator=(UniquePtr const &);

        在C++98中,unique_ptr的防拷贝方法是只声明不实现,而且声明也要用私有限定符。

        那么unique_ptr在析构时,释放方式由一个额外的删除器来完成//TODO

        综上所述,unique_ptr相对于auto_ptr更加安全,因为它增添了防拷贝机制,但是我们都知道在实际情况中,拷贝情况肯定是不可避免的,那么如果当我们遇到了拷贝情况,unique_ptr显然解决不了问题了,那么该如何做呢?

3.std::shared_ptrC++智能指针_第3张图片

                从C++11中开始增加了更加靠谱并支持拷贝的shared_ptr,shared_ptr通过一个引用计数的属性来支持指针对象的拷贝。那么何为引用计数呢?引用计数的原理是记录有多少个对象管理着这块资源,每个对象析构的时候对减计数,当有对象进行拷贝时加计数,由最后一个指向的对象来负责释放资源,所以当拷贝的对象增加时,此属性也需要增加,来记录现在有几个对象拷贝了自己。在对象被销毁时,也就是调用析构函数时,就说明自己不适用该资源了,对象的引用计数减一。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。反之就不能释放该资源,只能进行引用计数的减一,否则会造成野指针问题。

        注意:这里的引用计数并不能使用静态成员,因为静态的成员函数被全局所有对象共享,所以当使用引用计数使用静态成员时,所有的对象都可以对其进行更改,会造成混乱,所以我们应该一个资源配备一个引用计数,所以不能使用静态成员。

	template
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pRefCount(new int(1))
			,_pmtx(new mutex)
		{}

		shared_ptr(const shared_ptr& sp)
			:_ptr(sp._ptr)
			,_pRefCount(sp._pRefCount)
			,_pmtx(sp._pmtx)
		{
			AddRef();
		}

		void Release()
		{
			_pmtx->lock();
			bool flag = false;
			if (--(*_pRefCount) == 0 && _ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pRefCount;

				flag = true;
				//delete _pmtx; //err
			}
			_pmtx->unlock();
			if (flag == true)
			{
				delete _pmtx;
			}
		}

		void AddRef()
		{
			_pmtx->lock();
			++(*_pRefCount);
			_pmtx->unlock();
		}

		shared_ptr& operator=(const shared_ptr& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)
			{
				Release();
				_ptr = sp._ptr;
				_pRefCount = sp._pRefCount;
				_pmtx = sp._pmtx;
				AddRef();
			}
			return *this;
		}

		int use_count()
		{
			return *_pRefCount;
		}

		~shared_ptr()
		{
			Release();
		}
		//像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;

		}
		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pRefCount;
		mutex* _pmtx;
	};

        那么在多线程的环境下,当同时有多个执行流来访问shared_ptr并对其引用计数进行操作时,此时引用计数就变成了“临界资源”,也就是说在同一时间只能被一个执行流访问,在此期间如果有第二个执行流想访问它,只能排队,如果发生了同时更改,会引发线程安全的问题,所以我们还要在shared_ptr中加入锁,并且在shared_ptr对引用计数进行操作时候进行lock()和unlock(),以此来保证引用计数操作是原子的。

        但是shared_ptr也有弱点:

struct ListNode
{
	int val;
	//std::shared_ptr _next;
	//std::shared_ptr _prev;

	std::weak_ptr _next;
	std::weak_ptr _prev;

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

int main()
{
	std::shared_ptr n1(new ListNode);
	std::shared_ptr n2(new ListNode);

	//循环引用,死结
	//n1->_next = n2;
	//n2->_prev = n1;

	return 0;
}

        当用shared_ptr定义了两个链表节点,并且用链表的前驱和后继分别指向对方,在逻辑层面就会产生循环引用的场景,所谓循环引用就是两个智能指针互相指向的对方,也就是说这两个智能指针中每个引用计数都有对方,只有一方先释放后才会释放另一方,但是这个概念是相对的,究竟要谁先释放呢?这个问题shared_ptr无法处理,如果要解决这个问题,要引出一个新的智能指针weak_ptr。

4.std::weak_ptrC++智能指针_第4张图片

        这个智能指针严格意义上说并不算是智能指针,可以理解为它是专门为了解决shared_ptr循环引用问题而引出的。

        那么它是如何解决的呢?weak_ptr在使用时会写两个拷贝构造函数,参数都是shared_ptr,专门负责来管理shared_ptr中的资源,而且weak_ptr针对引用计数并不操作,

	template
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr& sp)
			:_ptr(sp.get())
		{}

		weak_ptr& operator=(const shared_ptr& sp)
		{
			_ptr = sp.get();

			return *this;
		}

		~weak_ptr()
		{}
	private:
		T* _ptr;
	};
struct ListNode
{
	int val;
	//std::shared_ptr _next;
	//std::shared_ptr _prev;

	std::weak_ptr _next;
	std::weak_ptr _prev;

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

int main()
{
	std::shared_ptr n1(new ListNode);
	std::shared_ptr n2(new ListNode);

	//循环引用,死结
	//n1->_next = n2;
	//n2->_prev = n1;

	//weak_ptr 不是常规意义上的智能指针,没有接收一个原生指针的构造函数,也不符合RAII
	//weak_ptr的next和prev对象可以访问指向节点资源,但是不参与节点资源释放,其实就是不增加计数

	return 0;
}

        

你可能感兴趣的:(c++,开发语言)