An article in C++ lets you know the charm of smart pointers

Recap

We know that in addition to static memory and stack memory, each program also has a memory pool, which is called free space or heap. Programs use the heap to store dynamically allocated objects, that is, objects that are allocated when the program is running. When dynamic objects are no longer used, our code must explicitly destroy them.

In C++, dynamic memory management is accomplished with a pair of operators: new and delete, new: allocate a space for an object in dynamic memory and return a pointer to the object, delete points to a dynamic exclusive pointer, Destroys the object, and frees the memory associated with it.

There are often two problems in dynamic memory management: one is forgetting to release memory, which will cause memory leaks; the other is releasing it when there is still a pointer referencing memory, and a pointer referencing illegal memory will be generated.

In order to use dynamic memory more easily (safer), the concept of smart pointers is introduced. A smart pointer behaves like a regular pointer, with the important difference that it is responsible for automatically releasing the pointed-to object

The principle of smart pointer

RAII : Use the object life cycle to control program resources. Mainly through the object's constructor to obtain resource management rights, and then through the destructor to release the managed resources. The principle is to entrust the responsibility of managing a resource to an object

//RAII
template<class T>
class SmartPtr
{
    
    
public:
	//构造函数获取资源管理权
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{
    
    }
	//通过析构函数释放资源
	~SmartPtr()
	{
    
    
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};
class A
{
    
    
private:
	int _a = 10;
};

void test()
{
    
    
	//错误写法
	int* ptr = new int[10];
	SmartPtr<int> sp(ptr);
	//立即初始化--申请资源时就立即绑定资源
	SmartPtr<int> sp2(new int);
	SmartPtr<A> sp3(new A);
}

This is not a smart pointer object, the smart pointer should meet the following conditions

  1. Realize RAII idea
  2. It is used in the same way as a pointer, for example, it needs to support * dereferencing and -> operations

Overloads for the following operations should be added to the class


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

One of the differences between smart pointers and ordinary pointers is that smart pointers do not need to manually free space

void test()
{
    
    
	//智能指针--编译器调用析构自动释放资源--不存在内存泄漏
	SmartPtr<A> sp(new A);
	(*sp)._a = 10;
	sp->_a = 100;

	//普通指针--手动释放内存
	int* p = new int;
	A* pa = new A;
	*p = 1;
	pa->_a = 10;
	//return  //提前结束普通指针就会导致内存泄漏
	delete p;
	delete pa;
}

The use of smart pointers in the C++ standard library

The smart pointers in the library are divided into auto_ptr , unique_ptr , share_ptr,
they all need to introduce header files #include <memory>to use

auto_ptr

auto_ptr is a flawed smart pointer ( disabled )

#include <memory>
using namespace std;
void test()
{
    
    
	auto_ptr<int> ap(new int);
	auto_ptr<int> ap2(new int(2));
	*ap = 10;
	*ap2 = 20;
}

When the auto_ptr pointer is assigned, the resource will be transferred, the purpose is to prevent multiple smart pointers from pointing to the same memory resource. But this design obviously does not meet our needs.
insert image description here
Let's simply simulate the implementation of auto_ptr to see how the bottom layer transfers resource rights.

//实现auto_ptr
template<class T>
class Auto_ptr
{
    
    
public:
	Auto_ptr(T* ptr)
		:_ptr(ptr)
	{
    
    }
	~Auto_ptr()
	{
    
    
		if (_ptr)
			delete _ptr;
	}

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

	Auto_ptr(Auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
    
    
		//资源管理权转移 
		ap._ptr = nullptr;
	}

	Auto_ptr<T>& operator=(Auto_ptr<T>& ap)
	{
    
    
		if (this != &ap)
		{
    
    
			if (_ptr)
				delete _ptr;
			//资源管理权转移 
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		return *this;
	}
private:
	T* _ptr;
};

unique_ptr

The unique_ptr smart pointer solves the problem of resource management authority transfer through anti-copying ---- set the unique_ptr assignment operator function and copy constructor as deletion functions

void test()
{
    
    
	unique_ptr<int> up(new int(10));
	unique_ptr<int> up2(up);//error
	unique_ptr<int> up3(new int(20));
	up = up3; //error
}

Reason for error: Both copy construction and assignment overloading functions are deleted functions.
insert image description here
The underlying implementation:

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

	Unique_ptr(const Unique_ptr<T>& up) = delete;
	Unique_ptr<T>& operator=(const Unique_ptr<T>& up) = delete;

	~Unique_ptr()
	{
    
    
		if (_ptr)
		{
    
    
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};

shared_ptr

shared_ptr is a new smart pointer provided in C++11, which not only solves the problem of resource management permission transfer, but also provides a reliable copy function

class A
{
    
    
public:
	int _a = 10;
	~A()
	{
    
    
		cout << "~A()" << endl;
	}
};

void test()
{
    
    
	shared_ptr<A> sp(new A);
	shared_ptr<A> sp2(new A);
	shared_ptr<A> sp3(sp2);//ok
	sp3 = sp;//ok
	sp->_a = 100;
	sp2->_a = 1000;
	sp3->_a = 10000;
	cout << sp->_a << endl;
	cout << sp2->_a << endl;
	cout << sp3->_a << endl;
}

Running results:
insert image description here
We found that as many resources are applied, as many resources will be released. At this time, sp and sp3 share a resource, and modifying sp3 is equivalent to modifying sp. So both end up printing 10000. Then share a resource, how to release the resource only once? ---- Reference count

We can use the interface provided by shared_ptr use_count()to check how many smart pointers are currently managing the same resource

void test()
{
    
    
	shared_ptr<A> sp(new A);
	cout << sp.use_count() << endl;//1
	shared_ptr<A> sp2(sp);
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	shared_ptr<A> sp3(new A);
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	cout << sp3.use_count() << endl;//1
	sp3 = sp;
	sp3 = sp2;
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	cout << sp3.use_count() << endl;//2
}

Running screenshot: The reason why there is a destructor call in the middle is because when sp3 points to sp, the reference count of sp3 is 0, and the destructor will be called to release resources. At this time, the resource created by sp has three smart pointers to manage
insert image description here
the diagram :
insert image description here
when implementing, we should ensure that a resource corresponds to only one counter, instead of each smart pointer having its own counter. So we can bind the resource and the counter together. At this time, the smart pointer pointing to the same resource accesses the same counter
member variable : the member variable should have two variables, namely the variable _ptr of the resource pointer and the variable _ptr of the resource pointer. The counter variable _countPtr, they are all pointer-type variable
copy constructors : in the copy constructor, the pointer of the current object should point to the resource of the object to be copied, and its counter should also be copied, and finally the counter needs to be Perform ++
assignment operator overloading : We cannot judge whether two objects are equal, but should assign values ​​only if the resources of the two objects are different. In the assignment, let the counter of the current object first - -, if it is 0, it means that the resource of the current object is only managed by the current object, and the resource needs to be released. Then modify the current object pointer to the resource of the object to be copied, and copy its counter. Finally, the counter must be ++ operated
Destructor : To judge the counter of the resource of the current object, perform – operation first, and then judge whether the counter is 0, if it is 0, the resource will be released, if it is not 0, then nothing will be done

template<class T>
class Shared_ptr
{
    
    
public:
	Shared_ptr(T* ptr)
		:_ptr(ptr)
		, _countPtr(new size_t(1))//初始化为1
	{
    
    }
	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _countPtr(sp._countPtr)
	{
    
    
		//计数器累加
		++(*_countPtr);
	}
	Shared_ptr<T> operator=(const Shared_ptr<T>& sp)
	{
    
    
		if (_ptr != sp._ptr)
		{
    
    
			//本身计数器自减
			//计数器为0,则当前对象需要释放资源
			if (--(*_countPtr) == 0)
			{
    
    
				delete _ptr;
				delete _countPtr;
			}

			_ptr = sp._ptr;
			_countPtr = sp._countPtr;

			++(*_countPtr);
		}
		return *this;
	}

	~Shared_ptr()
	{
    
    
		//计数器自减
		if (--(*_countPtr) == 0)
		{
    
    
			delete _ptr;
			delete _countPtr;
			_ptr = nullptr;
			_countPtr = nullptr;
		}
	}
	T& operator*()
	{
    
    
		return *_ptr;
	}
	T* operator->()
	{
    
    
		return _ptr;
	}
private:
	T* _ptr;
	size_t* _countPtr;//计数器指针
};

The shared_ptr smart pointer we implemented actually does not have thread safety issues in a multi-threaded scenario-the reference counter pointer is a shared variable, and multiple threads will cause counter confusion when they are modified. Cause resources to be released early or cause memory leaks
Let's take a look at the code, if it is safe, then the last destructor should only be called once

void fun(const Shared_ptr<A>& sp, int n)
{
    
    
	for (int i = 0; i < n; ++i)
		Shared_ptr<A> copy(sp);//创建copy智能指针
}

void test()
{
    
    
	Shared_ptr<A> sp(new A);
	int n = 100000;
	thread t1(fun, ref(sp), n);
	thread t2(fun, ref(sp), n);
	t1.join();
	t2.join();
}

Running result 1 : We found that the destructor of the object was not called, indicating that a memory leak occurred at this time.
insert image description here
Running result 2 : Calling the destructor twice means that a resource is released twice.
insert image description here
We can provide an interface to get the value of the counter in the class

	size_t getCount()
	{
    
    
		return *_countPtr;
	}

Then run in the code and get the value of the counter, and find that the value of the counter is not 0, so the destructor will not be called

insert image description here
So we can perform lock protection where the counter is modified. And this lock cannot be a global variable lock, and resources cannot be affected. Otherwise, when one resource is locked and modified, another resource will be affected, which will affect the execution efficiency of the code. A separate lock should be provided for each counter.
Here, both the ++ operation and the – operation are encapsulated.

template<class T>
class Shared_ptr
{
    
    
public:
	Shared_ptr(T* ptr)
		:_ptr(ptr)
		, _countPtr(new size_t(1))//初始化为1
		, _mtx(new mutex)
	{
    
    }
	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _countPtr(sp._countPtr)
		,_mtx(sp._mtx)
	{
    
    
		//计数器累加
		//++(*_countPtr);
		addCount();
	}
	Shared_ptr<T> operator=(const Shared_ptr<T>& sp)
	{
    
    
		if (_ptr != sp._ptr)
		{
    
    
			//本身计数器自减
			//计数器为0,则当前对象需要释放资源
			//if (--(*_countPtr) == 0)
			if (subCount() == 0)
			{
    
    
				delete _ptr;
				delete _countPtr;
				delete _mtx;
			}

			_ptr = sp._ptr;
			_countPtr = sp._countPtr;

			addCount();
		}
		return *this;
	}

	~Shared_ptr()
	{
    
    
		//计数器自减
		if (subCount() == 0)
		{
    
    
			delete _ptr;
			delete _countPtr;
			delete _mtx;
			_ptr = nullptr;
			_countPtr = nullptr;
			_mtx = nullptr;
		}
	}
	T& operator*()
	{
    
    
		return *_ptr;
	}
	T* operator->()
	{
    
    
		return _ptr;
	}

	size_t getCount()
	{
    
    
		return *_countPtr;
	}

	size_t addCount()
	{
    
    
		_mtx->lock();
		++(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}

	size_t subCount()
	{
    
    
		_mtx->lock();
		--(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}
private:
	T* _ptr;
	size_t* _countPtr;//计数器指针
	mutex* _mtx;
};

Running results : We found that in multi-threaded scenarios, it can also be released normally
insert image description here

circular reference problem

shared_ptr actually has some small problems, that is, the circular reference problem.
Let's take a look at the following code first

struct ListNode
{
    
    
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
	int _data;

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

void test()
{
    
    
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);

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

	n1->_next = n2;
	n2->_prev = n1;

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

Running results : We found that the resources are not released, and the counter is also increasing
insert image description here
. Illustration:
insert image description here
insert image description here

In C++11, specifically to solve this problem, a new smart pointer waek_ptr is introduced, which is called a weak pointer . When assigning or copying, the counter does not perform ++. The real resources will not be released during destructuring. Waek_ptr cannot be used alone, and its biggest role is to solve the problem of shared_ptr circular references.

struct ListNode
{
    
    
	weak_ptr<ListNode> _next;
	weak_ptr<ListNode> _prev;
	int _data;

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

void test()
{
    
    
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);

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

	n1->_next = n2;
	n2->_prev = n1;

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

Running results:
insert image description here
In our own shared_ptr, we only use delete to release resources when we release resources. In our way of applying for space, we don’t just use new to apply for space, and we may also use malloc to apply for space. At this time It should be released with free. So we also need to add a deleter to the smart pointer

void test()
{
    
    
	Shared_ptr<A> sp(new A[100]);//调用析构会报错
}

Deletors can be implemented primarily through functors

template<class T>
struct DeleteDel
{
    
    
	void operator()(T* ptr)
	{
    
    
		delete ptr;
	}
};

template<class T>
struct FreeDel
{
    
    
	void operator()(T* ptr)
	{
    
    
		free(ptr);
	}
};

template<class T>
struct DeleteArrDel
{
    
    
	void operator()(T* ptr)
	{
    
    
		delete[] ptr;
	}
};
template<class T, class Del = DeleteDel<T>>
class Shared_ptr
{
    
    
public:
	Shared_ptr(T* ptr)
		:_ptr(ptr)
		, _countPtr(new size_t(1))//初始化为1
		, _mtx(new mutex)
	{
    
    }
	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _countPtr(sp._countPtr)
		,_mtx(sp._mtx)
	{
    
    
		//计数器累加
		//++(*_countPtr);
		addCount();
	}
	Shared_ptr<T> operator=(const Shared_ptr<T>& sp)
	{
    
    
		if (_ptr != sp._ptr)
		{
    
    
			//本身计数器自减
			//计数器为0,则当前对象需要释放资源
			//if (--(*_countPtr) == 0)
			if (subCount() == 0)
			{
    
    
				//delete _ptr;
				//通过删除器来释放空间
				_del(_ptr);
				delete _countPtr;
				delete _mtx;
			}

			_ptr = sp._ptr;
			_countPtr = sp._countPtr;

			addCount();
		}
		return *this;
	}

	~Shared_ptr()
	{
    
    
		//计数器自减
		if (subCount() == 0)
		{
    
    
			//delete _ptr;
			//通过删除器来释放空间
			_del(_ptr);
			delete _countPtr;
			delete _mtx;
			_ptr = nullptr;
			_countPtr = nullptr;
			_mtx = nullptr;
		}
	}
	T& operator*()
	{
    
    
		return *_ptr;
	}
	T* operator->()
	{
    
    
		return _ptr;
	}

	size_t getCount()
	{
    
    
		return *_countPtr;
	}

	size_t addCount()
	{
    
    
		_mtx->lock();
		++(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}

	size_t subCount()
	{
    
    
		_mtx->lock();
		--(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}
private:
	T* _ptr;
	size_t* _countPtr;//计数器指针
	mutex* _mtx;
	Del _del;
};

insert image description here

Guess you like

Origin blog.csdn.net/qq_44443986/article/details/117415950