〖C++11〗Detailed explanation of smart pointers

The "Preface" article is about the smart pointer aspect of C++11 

"Belonging Column" C Quack

"Author" Mr. Maple Leaf (fy)

"Motto" Cultivate myself on the way forward

"Mr. Maple Leaf is a little intellectually ill"

"One sentence per article" 

Life is a train to the grave,

There will be many stops along the way,

It's hard to have someone walk with you from beginning to end.

When the person accompanying you wants to get off the car,

Even if you don't give up, you should be grateful.

Then wave goodbye.

—— Hayao Miyazaki

Table of contents

1. Why do we need smart pointers?

1.1 Memory leak problem

1.2 Use smart pointers to solve

1.3 The principle of smart pointer

Two, C++ smart pointer

2.1 std::auto_ptr

2.2 std::unique_ptr

 2.3 std::shared_ptr

2.4 shared_ptr thread safety issues

2.5 std::weak_ptr

3. The relationship between smart pointers in C++11 and boost


1. Why do we need smart pointers?

1.1 Memory leak problem

Regarding memory leaks, for example the following code:

#include <iostream>
using namespace std;

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	int* p1 = new int;
    int* p2 = new int;
	cout << div() << endl;
	delete p1;
    delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

When executing the above code, if the input divisor is 0, an exception will be thrown in the div function. At this time, the execution flow of the program will directly jump to the catch block in the main function for execution, which will eventually result in the memory requested in the Func function resource not released

In this case, we can first capture the exception thrown in the div function in the Func function, release the previously requested memory resource after capture, and then rethrow the exception, the code is as follows

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	int* p1 = new int;
    int* p2 = new int;
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
        delete p2;
		throw;
	}
	delete p1;
    delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

new will also throw an exception. If p1 here new throws an exception, then the execution flow of the program will directly jump to the catch block in the main function for execution. There is no problem here. Assuming that there is no problem with p1, what if p2's new also throws an exception? Need to nest another try and catch? ?

void Func()
{
	int* p1 = new int;
	int* p2 = nullptr;
	try
	{
		int* p2 = new int;//p2的new可能会抛异常
		try
		{
			cout << div() << endl;
		}
		catch (...)
		{
			delete p1;
			delete p2;
			throw;
		}
	}
	catch (...)
	{
		delete p1;
		throw;
	}
	delete p1;
	delete p2;
}

This kind of code looks very low, with crazy nesting of try and catch statements, so in order to solve this problem, smart pointers appear.

Supplement: Classification of memory leaks. In C/C++ programs, we generally care about two aspects of memory leaks:

  • Heap memory leak (Heap leak): Heap memory refers to a piece of memory allocated from the heap through malloc / calloc / realloc / new, etc. as needed during program execution. After use, it must be deleted by calling the corresponding free or delete. Assuming that the design error of the program causes this part of the memory to not be released, then this part of the space will no longer be used in the future, and Heap Leak will occur.
  • System resource leakage: Refers to the resources allocated by the system used by the program, such as sockets, file descriptors, pipes, etc., which are not released using the corresponding functions, resulting in a waste of system resources, which can seriously lead to reduced system performance and unstable system execution

Memory leaks are very common, and there are two solutions: 1. Pre-prevention type. Such as smart pointers , etc. 2. Post-event error checking type. such as leak detection tools

1.2 Use smart pointers to solve

What are smart pointers? ?

  • Hand over the requested memory space to a SmartPtr for management
  • When constructing a SmartPtr object, pass the memory space to be managed into the SmartPtr object
  • When the SmartPtr object dies, the destructor of SmartPtr will automatically release the managed memory space,
  • If an exception occurs, the requested space will be released with the life cycle of the SmartPtr object, and the problem of memory leaks can be well solved
  • In order for the SmartPtr object to be used like a native pointer, it is also necessary to overload the  *and operator ->

For example:

//智能指针
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		cout << "delete: " << _ptr << endl;
		delete _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

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

void Func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);
	cout << div() << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

In the above code, the requested memory space is handed over to a SmartPtr object sp1 and sp2 for management. In this way, no matter whether the program returns after normal execution, or returns halfway due to some reasons, or because an exception is thrown Returned, as long as the life cycle of the SmartPtr object ends, its corresponding destructor will be called to complete the release of memory resources.

1.3 The principle of smart pointer

(1)RAII

RAII (Resource Acquisition Is Initialization) is a simple technique that uses the object life cycle to control program resources (such as memory, file handles, network connections, mutexes, etc.)

Acquire resources when the object is constructed, then control access to the resources so that they remain valid throughout the life of the object, and finally release the resources when the object is destructed. In this way, we actually entrust the responsibility of managing a resource to an object.

This approach has two advantages:

  1. There is no need to explicitly release resources.
  2. In this way, the resources required by the object remain valid throughout its lifetime

for example:

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};

(2) Behavior like a pointer

The above (1) SmartPtr cannot yet be called a smart pointer, because it does not yet have the behavior of a pointer. The pointer can be dereferenced, and you can also access the content in the pointed space through ->, so: in the smart pointer , * and -> need to be overloaded, so that it can be used like a pointer

template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }
private:
	T* _ptr;
};

Summarize the principle of smart pointers:

  1. RAII characteristics
  2. Overload operator* and operator-> with pointer-like behavior 

But such a smart pointer is not perfect enough, there will be a problem of copying smart pointer objects, so there are different versions of smart pointers in C++

Solve the problem of copying smart pointer objects: For example, the smart pointer SmartPtr class implemented above, if a SmartPtr object is used to copy and construct another SmartPtr object, or a SmartPtr object is assigned to another SmartPtr object, the program will crash.

int main()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(sp1); //拷贝构造

	SmartPtr<int> sp3(new int);
	SmartPtr<int> sp4(new int);
	sp3 = sp4; //拷贝赋值
	
	return 0;
}

reason:

  • The copy constructor generated by the compiler by default completes the value copy (shallow copy) of the built-in type, so after using sp1 to copy and construct sp2, it is equivalent to the fact that sp1 and sp2 manage the same memory space, and when sp1 and sp2 are destructed, it will cause This space is freed twice.
  • The copy assignment function generated by the compiler by default also completes the value copy (shallow copy) of the built-in type, so after sp4 is assigned to sp3, it is equivalent to the space managed by sp3 and sp4 is the original space managed by sp3. When sp3 and sp4 are destructed It will cause this space to be released twice, and it will also cause the space originally managed by sp4 to not be released

Two, C++ smart pointer

2.1 std::auto_ptr

The smart pointer of auto_ptr is provided in the C++98 version of the library. The document introduces: std::auto_ptr

头文件:
#include <memory> 

auto_ptr solves the copying problem of smart pointers through the transfer of management rights , ensuring that only one object manages a resource at any time, and the same resource will not be released multiple times.

The test code is as follows:

int main()
{
	std::auto_ptr<int> ap1(new int(1));
	std::auto_ptr<int> ap2(ap1);
	*ap2 = 10;

	std::auto_ptr<int> ap3(new int(1));
	std::auto_ptr<int> ap4(new int(2));
	ap3 = ap4;

	return 0;
}

To debug and view:

After the management right of an object is transferred, it means that the object can no longer be used to access the originally managed resources, which will cause the object to be suspended, such as the above sp1 and sp2. If you continue to use these two objects, the program will directly crash , so you must understand its mechanism before using auto_ptr, otherwise the program is prone to problems.

auto_ptr is a failed design, and many companies have clearly stipulated that the use of auto_ptr is prohibited

Simple mock implementation of auto_ptr

namespace fy
{
	template<class T>
	class auto_ptr
	{
	public:
		//RAII
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			sp._ptr = nullptr;// 管理权转移
		}
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)// 检测是否为自己给自己赋值
			{
				// 释放当前对象中资源
				if (_ptr)
					delete _ptr;
				// 转移ap中资源到当前对象中
				_ptr = ap._ptr;
				ap._ptr = NULL;
			}
			return *this;
		}
		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

2.2 std::unique_ptr

unique_ptr is a smart pointer introduced in C++11. unique_ptr solves the copying problem of smart pointers by means of anti-copying , that is, simply and rudely prevents the copying of smart pointer objects, which also ensures that resources will not be released multiple times.

头文件:
#include <memory> 

Documentation introduction: unique_ptr

 test code

int main()
{
	std::unique_ptr<int> up1(new int(1));
	std::unique_ptr<int> up2(up1); //error,不允许拷贝
	return 0;
}

compile error

unique_ptr simple analog implementation 

namespace fy
{
	template<class T>
	class unique_ptr
	{
	public:
		// RAII
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//防拷贝
		unique_ptr(const unique_ptr<T>&sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
	private:
		T* _ptr;
	};
}

 2.3 std::shared_ptr

shared_ptr is a smart pointer introduced in C++11. shared_ptr solves the copying problem of smart pointers by means of reference counting, that is to say, shared_ptr supports copying

头文件:
#include <memory> 

 Documentation introduction: shared_ptr

 The principle of shared_ptr: share resources between multiple shared_ptr objects by means of reference counting

  • shared_ptr maintains a count for each resource inside it, which is used to record that the resource is shared by several objects.
  • When the object is destroyed (that is, the destructor is called), it means that the resource is no longer used, and the reference count of the object is reduced by one.
  • If the reference count is 0, it means that it is the last object to use the resource and must release the resource;
  • If it is not 0, it means that there are other objects using the resource besides yourself, and the resource cannot be released, otherwise other objects will become wild pointers

Through this reference counting method, multiple objects can be supported to manage a certain resource together, that is, the copy of the smart pointer is supported, and the resource will be released only when the reference count corresponding to a resource is reduced to 0, thus ensuring the same A resource will not be freed more than once

Test code:

// 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
int main()
{
	shared_ptr<int> sp1(new int(1));
	shared_ptr<int> sp2(sp1);
	*sp1 = 2;
	*sp2 = 3;
	//use_count成员函数,用于获取当前对象管理的资源对应的引用计数
	cout << sp1.use_count() << endl;

	shared_ptr<int> sp3(new int(1));
	shared_ptr<int> sp4(new int(2));
	shared_ptr<int> sp5(new int(3));
	sp4 = sp3;
	sp5 = sp3;
	cout << sp3.use_count() << endl;
	return 0;
}

operation result

 

debug view

Shared_ptr simple simulation implementation

  1. A member variable count needs to be added to indicate the reference count corresponding to the resource managed by the smart pointer object
  2. Apply for resources in the constructor, open up on the heap, and initialize the reference count, set it to 1, indicating that only one object is currently managing this resource
  3. In the copy constructor, when copying once, the reference count corresponding to the resource needs to be ++ at the same time
  4. In the copy assignment function, first count the reference count corresponding to the resource managed by the current object -- (if it is reduced to 0, it needs to be released), and then manage the resource it manages together with the incoming object, and at the same time need the reference corresponding to the resource Count ++.
  5. In the destructor, the reference count corresponding to the management resource --, if it is reduced to 0, the resource needs to be released
namespace fy
{
	template<class T>
	class shared_ptr
	{
	public:
		// (1)RAII
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pCount(new int(1))
		{}
		~shared_ptr()
		{
			if (--(*_pCount) == 0)
			{
				if (_ptr != nullptr)
				{
					std::cout << "_ptr: " << _ptr << std::endl;
					delete _ptr;
					_ptr = nullptr;
				}
				delete _pCount;
				_pCount = nullptr;
			}
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pCount(sp._pCount)
		{
			++(*_pCount);
		}

		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)//管理同一块空间的对象之间无需进行赋值操作
			{
				if (--(*_pCount) == 0)
				{
					std::cout << "operator= delete: " << _ptr << std::endl;
					delete _ptr;
					delete _pCount;
				}
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				++(*_pCount);
			}
			return *this;
		}
		//获取引用计数
		int use_count()
		{
			return *_pCount;
		}
		// (2)可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;//管理的资源
		int* _pCount; //管理的资源对应的引用计数
	};

test run

 

Note: The reference count count in shared_ptr cannot simply be defined as a member variable of type int, because this means that each shared_ptr object has its own count member variable, and when multiple objects want to manage the same resource, These objects should use the same reference count

2.4 shared_ptr thread safety issues

The shared_pt r simulated above also has thread safety issues. Since the reference counts of multiple objects that manage the same resource are shared, multiple threads may simultaneously increment or decrement the same reference count . Both self-increment and self-decrement operations are not atomic operations , so the reference count needs to be protected by locking, otherwise it will lead to thread safety issues

So you need to lock the ++ -- operation in the code

Modify the code as follows:

#pragma once
#include <iostream>
#include <memory>
#include <mutex>

namespace fy
{
	template<class T>
	class shared_ptr
	{
	public:
		// (1)RAII
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pCount(new int(1))
			, _mutex(new mutex)
		{}
		//对++操作进行加锁
		void Add()
		{
			_mutex->lock();
			(*_pCount)++;
			_mutex->unlock();
		}
		//对--操作进行加锁
		void Release()
		{
			_mutex->lock();
			bool flag = false;
			if (--(*_pCount) == 0) //将管理的资源对应的引用计数--
			{
				if (_ptr != nullptr)
				{
					cout << "delete: " << _ptr << endl;
					delete _ptr;
					delete _pCount;
					_ptr = nullptr;
					_pCount = nullptr;
					flag = true;
				}
			}
			_mutex->unlock();
			if (flag == true)//释放锁
			{
				delete _mutex;
			}
		}
		~shared_ptr()
		{
			Release();
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pCount(sp._pCount)
			, _mutex(sp._mutex)
		{
			Add();
		}

		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)//管理同一块空间的对象之间无需进行赋值操作
			{
				Release();
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				_mutex = sp._mutex;
				Add();
			}
			return *this;
		}
		//获取引用计数
		int use_count()
		{
			return *_pCount;
		}
		// (2)可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;//管理的资源
		int* _pCount; //管理的资源对应的引用计数
		mutex* _mutex; //管理的资源对应的互斥锁
	};
}
  • In the Release function, when the reference count is reduced to 0, the mutex resource needs to be released, but the mutex cannot be released in the critical section, because the unlock operation needs to be performed later, so a flag variable is used in the code, through the flag Variables to determine the need to release mutex resources after unlocking
  • shared_ptr only needs to ensure the thread safety of reference counting, not the thread safety of managed resources
  • The problem of thread safety pointing to resources on the heap is handled by the person who accesses it, and the smart pointer does not care about it, nor can it control it

Custom deleter for smart pointers

When the life cycle of the smart pointer object ends, all smart pointers deleterelease resources by default, which is not appropriate, because the smart pointer does not only manage the newmemory space applied by the method, but the smart pointer manages It is also possible to new[]apply for the space in the way, or manage a file pointer. new[]The memory space applied in the way must delete[]be released in the way, and the file pointer must fclosebe released by calling the function

Custom deleters are implemented through functors, Lambda expressions, or function pointers. Here is a brief introduction. Custom deleters are very complicated to implement.

2.5 std::weak_ptr

There is a fatal flaw in shared_ptr: circular reference . In order to solve this problem, weak_ptr is generated. weak_ptr is a smart pointer introduced in C++11, weak_ptr is not used to manage the release of resources, weak_ptr is a supplement to shared_ptr.

头文件:
#include <memory> 

Documentation introduction: weak_ptr

circular reference problem

The circular reference problem of shared_ptr will only occur in some specific scenarios.

For example, define the following node class: create two new nodes on the heap, connect the two nodes, and finally release the two nodes

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode(){ cout << "~ListNode()" << endl;}
};

int main()
{
	//新建节点
	ListNode* node1 = new ListNode;
	ListNode* node2 = new ListNode;

	node1->_next = node2;
	node2->_prev = node1;
	//释放
	delete node1;
	delete node2;
	return 0;
}

There is no problem with the above procedure, and both nodes can be released correctly. In order to prevent the node from being released due to reasons such as returning or throwing an exception in the middle of the program, we hand over the two nodes to two shared_ptr objects for management. The types of the next and prev member variables in the ListNode class are also changed to the shared_ptr type

struct ListNode
{
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
	int _val;
	~ListNode() { cout << "~ListNode()" << endl; }
};

int main()
{
	//新建节点
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	node1->_next = node2;
	node2->_prev = node1;
	return 0;
}

At this time, the two nodes are not released after the program runs, but if any one of the two lines of code when connecting the nodes is removed, then the two nodes can be released correctly. The fundamental reason is because of these two lines The code connecting the nodes caused a circular reference

Circular reference analysis:

  1. The two smart pointer objects node1 and node2 point to two nodes, the reference count becomes 1, and we don't need to manually delete.
  2. The _next of node1 points to node2, the _prev of node2 points to node1, and the reference count becomes 2.
  3. Node1 and node2 are destroyed, and the reference count is reduced to 1, but _next still points to the next node. But _prev also points to the previous node.
  4. That is to say, _next is destructed, and node2 is released.
  5. That is to say, _prev is destructed, and node1 is released.
  6. But _next is a member of node, node1 is released, _next will be destructed, and node1 is managed by _prev, _prev is a member of node2, so this is called a circular reference, and no one will release it

 

weak_ptr is a smart pointer introduced in C++11, weak_ptr is not used to manage the release of resources, it is mainly used to solve the circular reference problem of shared_ptr

  • Principle: weak_ptr supports using shared_ptr objects to construct weak_ptr objects. The constructed weak_ptr objects and shared_ptr objects manage the same resource, but the reference count corresponding to this resource will not be increased. 

 In the scenario of reference counting, just change the _prev and _next in the node to weak_ptr. The principle is, node1->_next = node2; and node2->_prev = node1; when the _next and _prev of weak_ptr are different Will increase the reference counts of node1 and node2.

Modify the code as follows:

struct ListNode
{
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	int _val;
	~ListNode() { cout << "~ListNode()" << endl; }
};

int main()
{
	//新建节点
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

Compile and run, resources are released normally

 

 weak_ptr simple simulation implementation

shared_ptr also provides a get function to obtain the resources it manages, assisting weak_ptr

T* get() const
{
	return _ptr;
}

The weak_ptr simulation implementation code is as follows:

  1. weak_ptr supports copying and constructing weak_ptr objects with shared_ptr objects, and obtains resources managed by shared_ptr objects during construction
  2. weak_ptr supports copying and assigning shared_ptr objects to weak_ptr objects, and obtaining resources managed by shared_ptr objects when assigning values
template<class T>
	class weak_ptr
	{
	public:
		// RAII
		weak_ptr()
			:_ptr(nullptr)
		{}
		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())//weak_ptr支持用shared_ptr对象拷贝构造weak_ptr对象,构造时获取shared_ptr对象管理的资源。
		{}
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();//weak_ptr支持用shared_ptr对象拷贝赋值给weak_ptr对象,赋值时获取shared_ptr对象管理的资源
			return *this;
		}
		// (2)可以像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};

3. The relationship between smart pointers in C++11 and boost

  1. The first smart pointer auto_ptr was created in C++98.
  2. C++ boost gives more practical scoped_ptr and shared_ptr and weak_ptr.
  3. C++ TR1, introduced shared_ptr, etc. But note that TR1 is not a standard version.
  4. C++11, introduced unique_ptr and shared_ptr and weak_ptr. It should be noted that unique_ptr corresponds to boost's scoped_ptr. And the implementation principles of these smart pointers refer to the implementation in boost
  5. The boost library is a general term for some C++ libraries that provide extensions to the C++ language standard library. One of the original intentions of the boost library community is to provide reference implementations for the standardization of C++. For example, in the C++ standard library TR1 submitted for review, there is Ten boost libraries became candidates for the standard library.

--------------------- END ---------------------- 

「 作者 」 枫叶先生
「 更新 」 2023.5.13
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。

Guess you like

Origin blog.csdn.net/m0_64280701/article/details/130331363