foreword
C++ does not have a garbage collection mechanism, and the resources of new or malloc need to be released manually. But sometimes forgetting to release occurs, which can lead to memory leaks or exception safety issues.
C++ introduces smart pointers to solve the above problems.
One: RAII
RAII is a simple technique for leveraging object lifetimes to control program resources such as memory, file handles, network connections, mutexes, and so on.
Acquire resources when the object is constructed, then control access to the resource to keep it valid for the lifetime of the object, and finally release the resource when the object is destructed. We actually delegate the responsibility for managing a resource to an object.
- No need to explicitly release resources.
- In this way, the resources required by the object remain available for the lifetime of the object.
Code demo:
#include<iostream> using namespace std; // type indeterminate template<class T> class SmartPtr{ // Help us manage the release of resources public: SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr(){ if (_ptr){ cout << "delete: " << _ptr << endl; delete _ptr; } } private: T* _ptr; }; int div(){ int a, b; cin >> a >> b; if (b == 0){ throw invalid_argument("divide by 0 error"); } return a / b; } void func(){ int* ptr = new int; // Whether the function ends normally or throws an exception, it will cause the destructor to be called after the life cycle of the sp object expires. SmartPtr<int> sp(ptr); cout << div() << endl; } int main(){ try{ func(); } catch (exception& e){ cout << e.what() << endl; } return 0; }
Two: smart pointer
RAII is an idea of resource management, and smart pointers are realized by relying on the idea of RAII.
2.1 The principle of smart pointer
- RAII characteristics
- It has pointer like behavior and overloads operator* and operator->
2.2 std::auto_ptr
The C++98 version of the library provides a smart pointer for auto_ptr.
Code example:
#include<iostream> using namespace std; namespace WJL{ template<class T> class auto_ptr{ // Help us manage the release of resources public: // Constructor saves resources auto_ptr(T* ptr) :_ptr(ptr) {} // Destructor releases resources ~auto_ptr(){ if (_ptr){ cout << "delete: " << _ptr << endl; delete _ptr; _ptr = nullptr; } } // management transfer auto_ptr(auto_ptr<T>& ap) :_ptr(ap._ptr) { ap._ptr = nullptr; } // Assignment s1 = s2 auto_ptr<T>& operator=(const auto_ptr<T>& ap){ if (this != &ap){ auto_ptr<T> PtrTemp(ap); T* pTemp = PtrTemp._ptr; PtrTemp._ptr = _ptr; _ptr = pTemp; } return *this; } T& operator*(){ return *_ptr; } T* operator->(){ return _ptr; } private: T* _ptr; }; } int main(){ WJL::auto_ptr<int> sp1(new int); WJL::auto_ptr<int> sp2 = sp1; return 0; }
The realization principle of auto_ptr: the idea of management right transfer, if a copy occurs, the resources in the ap are transferred to the current object, and then the other ap is disconnected from the resources it manages.
This solves the problem of program crash caused by a space being used by multiple objects.
But let's analyze it from the principle level. After copying, setting the pointer of the ap object to null will cause the ap object to hang in the air, and there will be problems when accessing resources through the ap object.
2.3 std::unique_ptr
More reliable unique_ptr smart pointers are available in C++11.
Code example:
#include<iostream> using namespace std; namespace WJL{ // Copy protection (simple and rude) template<class T> class unique_ptr{ // Help us manage the release of resources public: // Constructor saves resources unique_ptr(T* ptr) :_ptr(ptr) {} // Destructor releases resources ~unique_ptr(){ if (_ptr){ cout << "delete: " << _ptr << endl; delete _ptr; _ptr = nullptr; } } unique_ptr(unique_ptr<T>& up) = delete; unique_ptr<T>& operator = (unique_ptr<T>& up) = delete; T& operator*(){ return *_ptr; } T* operator->(){ return _ptr; } private: T* _ptr; }; } int main(){ WJL::unique_ptr<int> up1(new int); // WJL::unique_ptr<int> up2 = up1; return 0; }
The design idea of unique_ptr is very simple and rude: unique_ptr is copy-proof, that is, it does not allow copying and assignment.
However, there will be problems when using it in scenarios that require copying.
2.4 std::shared_ptr
More reliable shared_ptr smart pointers that support copying are available in C++11.
#include<iostream> using namespace std; namespace WJL{ // A smart pointer that supports copying, shared_ptr. template<class T> class shared_ptr{ // Help us manage the release of resources public: // Constructor saves resources shared_ptr(T* ptr) :_ptr(ptr) , p_count(new int(1)) {} shared_ptr(shared_ptr<T>& sp) :_ptr(sp._ptr) , p_count(sp.p_count) { ++(*p_count); } shared_ptr<T>& operator=(shared_ptr<T>& sp){ if (this != &sp){ if (--(*p_count) == 0){ delete _ptr; delete p_count; } _ptr = sp._ptr; p_count = sp.p_count; ++(*p_count); } return *this; } // Destructor releases resources ~shared_ptr(){ if (--(*p_count) == 0 && _ptr){ cout << "delete: " << _ptr << endl; delete _ptr; _ptr = nullptr; delete p_count; p_count = nullptr; } } T& operator*(){ return *_ptr; } T* operator->(){ return _ptr; } private: T* _ptr; int* p_count; // Reference counting (records how many objects share management resources together) }; } int main(){ WJL::shared_ptr<int> sp1(new int); WJL::shared_ptr<int> sp2 = sp1; return 0; }
The principle of shared_ptr is to realize the sharing of resources among multiple shared_ptr objects by means of reference counting.
shared_ptr internally maintains a counter for each resource to record that the resource is shared by several objects. When the object is destroyed (that is, the destructor is called), it means that it does not use the resource, 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 the resource must be released. If it is not 0, it means that there are other objects that are using the resource except yourself, and the resource cannot be released, otherwise other objects will become wild pointers.
Three: Thread safety of std::shared_ptr
shared_ptr thread safety is divided into two aspects:
- The reference count in the smart pointer object is shared by multiple smart pointer objects. The reference counts of the two threads to the smart pointer are added or subtracted at the same time. This operation is not atomic, which may cause the problem that the resource is not released or the program crashes.
- Objects managed by smart pointers are stored on the heap, and simultaneous access by two threads will lead to thread safety issues.
Code example:
The addition and subtraction of reference counts are locked to ensure thread safety.
namespace WJL{ template<class T> class shared_ptr{ public: shared_ptr(T* ptr = nullptr) :_ptr(ptr) , _pcount(new int(1)) , _pmtx(new mutex) {} shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) , _pcount(sp._pcount) , _pmtx(sp._pmtx) { add_ref_count(); } // sp1 = sp4 shared_ptr<T>& operator=(const shared_ptr<T>& sp){ if (this != &sp){ // decrement the reference count and release the resource if I am the last object managing the resource release(); // I started managing resources with you _ptr = sp._ptr; _pcount = sp._pcount; _pmtx = sp._pmtx; add_ref_count(); } return *this; } void add_ref_count(){ _pmtx->lock(); ++(*_pcount); _pmtx->unlock(); } void release(){ bool flag = false; _pmtx->lock(); if (--(*_pcount) == 0){ if (_ptr){ cout << "delete:" << _ptr << endl; delete _ptr; _ptr = nullptr; } delete _pcount; _pcount = nullptr; flag = true; } _pmtx->unlock(); if (flag == true){ delete _pmtx; _pmtx = nullptr; } } ~shared_ptr(){ release(); } int use_count(){ return *_pcount; } T* get_ptr() const{ return _ptr; } T& operator*(){ return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; // Record how many objects share management resources together, and the last destructor releases resources int* _pcount; mutex* _pmtx; }; }
Four: The circular reference problem of std::shared_ptr
Analysis of circular references:
- The two smart pointer objects node1 and node2 point to two nodes, the reference count becomes 1, and we do not need to delete manually.
- _next of node1 points to node2, _prev of node2 points to node1, and the reference count becomes 2.
- _next is destructed, node2 is released, _prev is destructed, node1 is released.
- _next is a member of node1, and _next will be destructed when node1 is released, while node1 is managed by _prev, and _prev is a member of node2.
This is called a circular reference, and no one will release it.
4.1 The solution to circular references
Solution: In the case of reference counting, change _prev and _next in the node to weak_ptr.
Principle analysis: node1->_ next = node2; And node2->_ prev = node1; Hour weak_ptr_ Next and_ Prev does not increase the reference count of node1 and node2
Code demo:
namespace WJL{ // Strictly speaking, weak_ptr is not a smart pointer, because he has no RAII resource management mechanism // Specifically solve the circular reference problem of shared_ptr template<class T> class weak_ptr{ public: weak_ptr() = default; weak_ptr(const shared_ptr<T>& sp) :_ptr(sp.get_ptr()) {} weak_ptr<T>& operator = (const shared_ptr<T>& sp){ _ptr = sp.get_ptr(); return *this; } T& operator*(){ return *_ptr; } T* operator->(){ return _ptr; } private: T* _ptr; }; }
summary
C++ does not have a garbage collection mechanism, and it is a problem that the requested resources need to be released, especially when encountering exception security issues, memory leaks will occur if you are not careful. Memory leaks will lead to less and less memory available to the program, making the program paralyzed, so we should try to avoid the problem of memory leaks.
This has developed a smart pointer based on the RAII idea.