C++ grammar - explain the concept, implementation principle and defects of smart pointers in detail

Table of contents

1. The concept of smart pointer

(1). History of smart pointers

(2). The use of smart pointers

episode.auto_ptr

①unique_ptr

②shared_ptr

③weak_ptr

2. Realization of smart pointer

3. Defects of smart pointers

(1). Circular reference

(2). Customized deleter


1. The concept of smart pointer

The smart pointer refers to the RAII (Resource Acquisition Is Initialization, that is, resource acquisition is initialization) technology, eliminating the need to manually release the pointer to the resource, hoping to automatically release the memory resource. At the bottom, it is to release resources by encapsulating a class and using a destructor.

When the class object is out of scope and destroyed, the destructor will be called automatically to complete the release of resources.

(1). History of smart pointers

The original smart pointer was auto_ptr proposed by C++98, but because of the large defects in use, the boost community (specifically for C++ programmers) established by Beman G. Dawes (one of the members of the C++ committee) provides many free and easy-to-use self-made library) contributed scoped_ptr, shared_ptr, weak_ptr to be adopted by C++11, and modified to official unique_ptr, shared_ptr, weak_ptr.

(2). The use of smart pointers

header file <memory>

ps: Although the following auto_ptr and unique_ptr are all library template classes, they are all called pointers for easy understanding. 

episode.auto_ptr

For auto_ptr, the editor thinks it is very tasteless. Although it can also automatically release resources, once the class is copied, the resources will be transferred to the copy object, and the copied object will lose the resources (much like a mobile copy). Therefore, once there is a subsequent call to the copied object, it will cause a huge security problem.

①unique_ptr

As the name implies, a "unique" pointer means that there can only be one pointer pointing to the requested resource space, and there are no two pointers pointing to the same space at the same time. It can be said that this is the improvement of auto_ptr in C++11, and it is used as a smart pointer a form.

Unlike auto_ptr, unique_ptr prohibits copying behavior, and of course assignment is also prohibited .

②shared_ptr

"Shareable" pointers indicate that copy and assignment behaviors are allowed, that is, multiple pointers are allowed to point to the same space.

At the same time, no matter how many pointers point to a space, this space will only be released once. 

std::shared_ptr<int> p1(new int);
std::shared_ptr<int> p2;
p2 = p1;//不会有任何问题,资源只会释放一次

③weak_ptr

"Unauthorized" pointer (I think it is better to translate it as unauthorized, not weak) , this pointer will not release the resources pointed to, and is used to solve some defects of shared_ptr (circular reference), the third part will explain in detail .

2. Realization of smart pointer

 Here we focus on implementing shared_ptr.

First define a template class, one of the pointer members is responsible for pointing to the requested space, delete the pointer member in the destructor to complete the automatic release of resources .

But the difficulty lies in how to copy. If it is simply copied, it will cause the problem of repeated release of resources.

So we use reference counting to solve it.

Specially and dynamically open up an int space to record how many pointers point to the resource space we applied for.

When calling construction, copying, and assignment, count ++, and when calling destructor, count --, until the count space value is 0, the resource is actually released .

It is worth noting that the assignment needs to consider the original resource space of the assignee and the original counting space.

code show as below:

template<class T>
class Shared_ptr
{
	typedef Shared_ptr<T> Self;
private:
    bool Destory()//销毁
	{
		if (pCount == nullptr || (*pCount) == 0) return false;

		(*pCount)--;//计数--

		if ((*pCount) == 0 && _ptr)
		{
			delete _ptr;
			delete pCount;
		}
		_ptr = nullptr;
		pCount = nullptr;
		return true;
	}
public:
	Shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, pCount(new int(1))
	{}
	Shared_ptr(const Self& sp)//拷贝构造
		:_ptr(sp._ptr)
		, pCount(sp.pCount)
	{
		(*pCount)++;
	}

	Self& operator=(const Self& sp)//赋值
	{
		if (_ptr == sp._ptr) return *this;
		Destory();
		_ptr = sp._ptr;
		pCount = sp.pCount;
		(*pCount)++;
		return *this;
	}
	bool release()//释放本指针空间(还有其他指向时不释放空间)
	{
		return Destory();
	}
	~Shared_ptr()//析构
	{
		std::cout << "~" << std::endl;
		Destory();
	}

	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T* get()
	{
		return _ptr;
	}
    bool expired()//检查是否过期(指向空间是否已被释放)
	{
        
		return pCount != nullptr && *pCount == 0;
	}
	size_t get_count() const
	{
		return *pCount;
	}
private:
	T* _ptr;//指向空间
	int* pCount;//指向计数空间
};

3. Defects of smart pointers

(1). Circular reference

shared_ptr hides a huge bug.

Take a look at the following code:

class A {
public:
	~A() {
		std::cout << "析构" << std::endl;
	}
	std::shared_ptr<A> next;
	std::shared_ptr<A> prev;
	int i = 0;
};

int main()
{
	
	std::shared_ptr<A> p1(new A);
	std::shared_ptr<A> p2(new A);
	p1->next = p2;
	p2->prev = p1;

 	return EXIT_SUCCESS;
}

 Under normal circumstances, "destruction" should be printed twice, respectively when calling the p1 and p2 destructor delete to point to the space.

But actually none of them are printed:

This is because of a circular reference situation.

The next of the space pointed to by p1 points to the p2 space, causing the count of the p2 space to become 2. Similarly, the prev of the space pointed to by p2 points to the p1 space, and the count of the p1 space becomes 2.

However, only p1 and p2 are popped out of the stack when they are destructed, that is, the destructors are called once, and the counts of the two spaces are both -1 and become 1. At this time, the space will not be really released, but the pointers to these two spaces cannot be found "on the bright side". Because these two heap spaces are respectively pointed to by pointers on the other heap space, it can be regarded as a kind of "deadlock".

The drawing is as follows:

Solution:

Specifically define a template class smart pointer, which can only point to the space but cannot change the number of counts .

That is weak_ptr (that's how the name comes from).

code show as below:

ps: The editor uses weak_ptr as a friend class of shared_ptr, or it can be a common template class.

class Shared_ptr
{
	typedef Shared_ptr<T> Self;
	template<class T>
	friend class Weak_ptr;
    . . .
}

template<class T>
class Weak_ptr
{
	typedef Weak_ptr<T> Self;
	typedef Shared_ptr<T> Sptr;
public:
	Weak_ptr()
		:_ptr(nullptr)
	{}
	Weak_ptr(const Self& wp)
		:_ptr(wp._ptr)
		,pCount(wp.pCount)
	{}
	Weak_ptr(const Sptr& sp)
		:_ptr(sp._ptr)
		,pCount(sp.pCount)
	{}
	~Weak_ptr()
	{}
	Self& operator=(const Self& wp)
	{
		_ptr = wp._ptr;
		pCount = wp.pCount;
		return *this;
	}
	Self& operator=(const Sptr& sp)
	{
		_ptr = sp._ptr;
		pCount = sp.pCount;
		return *this;
	}
	bool expired()
	{
		return *pCount == 0;
	}
	T& operator*()
	{
		assert(!expired());
		return *_ptr;
	}
	T* operator->()
	{
		assert(!expired());
		return _ptr;
	}
private:
	T* _ptr;
	int* pCount;
};

(2). Customized deleter

If you are careful, you may find that the destructor has only one way of delete, so what if we need delete[]?

For example in this case:

std::shared_ptr<A> p1(new A[5]);

At this time, an error will be reported:

But if it is a built-in type such as int, no error will be reported.

This is because there is a destructor, and if the custom type (weak_ptr) does not have a destructor, no error will be reported.

Let's sort it out carefully:

If the custom type does not have a destructor, then the compiler only needs to treat it as a built-in type.

But for a custom type with a destructor, the compiler will open 4 bytes before the space to record the number of the type opened according to the number of new spaces. When delete[] is called, the corresponding destructor will be called according to this record.

But because we are using delete, according to the record, it should be called n times, and delete will only be called once, which conflicts with the record and an error is reported.

Solution:

According to the object to be deleted, determine the need for delete or delete[], pass a functor to the smart pointer template, and call the functor when deleting.

code show as below:

template<class T>//默认仿函数,默认使用delete
class DefaultDelete {
public:
	void operator()(T* _ptr) {
		delete _ptr;
	}
};

template<class T, class D = DefaultDelete<T>>//传一个默认的仿函数
class Shared_ptr
{
private:
    bool Destory()//销毁
	{
		. . .
		if ((*pCount) == 0 && _ptr)
		{
			D()( _ptr);
			delete pCount;
		}
		. . .
	}
    . . .
}

template<class T>//可以自制一个传delete[]
class Free {
public:
	void operator()(T* _ptr) {
		delete[] _ptr;
	}
};

It is worth noting that the official shared_ptr is to pass the functor object in the constructor, and unique_ptr is to pass the functor type to the template parameter.

I'm not a great programmer, I'm just a good programmer with good habits - Kent Beck


Please correct me if there is any mistake 

Guess you like

Origin blog.csdn.net/weixin_61857742/article/details/127975548
Recommended