Foreword:
- In this issue, what we are going to learn is the new concept proposed in C++11-exception pointers!
Table of contents
(1) Introduction of smart pointers
1. What is a memory leak and the dangers of a memory leak?
2. Memory leak classification (understanding)
3. How to detect memory leaks (understand)
(3) The use and principle of smart pointers
(4) The relationship between smart pointers in C++11 and boost
(1) Introduction of smart pointers
The applied space (that is, the space created by new ) needs to be deleted at the end of use , otherwise memory fragmentation will occur. During the running of the program, the new object is deleted in the destructor , but this method cannot solve all problems, because sometimes new occurs in a global function, and this method will cause mental burden on the programmer. At this point, smart pointers come in handy . Using smart pointers can avoid this problem to a large extent, because a smart pointer is a class. When the scope of the class is exceeded, the class will automatically call the destructor, and the destructor will automatically release resources. Therefore, the working principle of smart pointers is to automatically release memory space when the function ends, avoiding manual release of memory space.
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
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;
}
【explain】
-
p1
p1
The pointer will remain a null pointer because the pointer has not yet been allocated a valid memory address before the exception is thrown . -
Since the pointer has not been allocated valid memory, it is unsafe
p1
to delete in subsequent code .p1
-
p2
The pointer will also not be allocated valid memory, because afterp1
the exception is thrown, the program flow will jump directly to the exception handling block without continuingp2
the allocation. -
div()
Exceptions in the function will be thrown andmain()
caught in the function's exception handling block.
new int
an exception is thrown during the memory allocation process, the pointer
p1
and
p2
will remain uninitialized, and the allocated memory will not be released, resulting in a memory leak.
(2) Memory leaks
1. What is a memory leak and the dangers of a memory leak?
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
【explain】
In the above code, several memory leaks and exception security issues are involved. Let me briefly analyze it:
Memory is allocated but forgotten to be released:
- In the following lines,
malloc
memory is allocated, but no correspondingfree
memory is released:
int* p1 = (int*)malloc(sizeof(int));
- Likewise, in the following lines, use is made
new
to allocate memory, but there is no corresponding usedelete
to free the memory:
int* p2 = new int;
- These memory allocations are not freed, causing memory leaks, and the allocated memory is not released even after the program ends.
Exceptional security issues:
- In the following lines, use is used
new
to allocate an array of integers, but there is no corresponding waydelete[]
to free the memory:
int* p3 = new int[10];
- When
Func()
an exception occurs inside a function,delete[] p3
it will not be executed, resulting in a memory leak when the function returns.
Memory leak caused by exception:
- In
Func()
the function, if an exception occurs,delete p1
anddelete p2
will not be executed, causing the memory allocated byp1
and not to be released.p2
In order to solve these problems, everyone needs to:
free
Use or to free memory when appropriatedelete
to avoid memory leaks.- After allocating memory, make sure to use
try
blocks so that the allocated memory can be freed when an exception occurs. - Be careful when handling exceptions to ensure that you do not re-free memory that has already been freed.
In summary, writing robust code requires attention to resource management and exception handling to ensure that memory leaks and other exception issues are minimized.
2. Memory leak classification (understanding)
In C/C++ programs, we generally care about two aspects of memory leaks:
- Heap memory refers to a piece through malloc / calloc / realloc / new , etc. according to needs during program execution. After use, it must be deleted by calling the corresponding free or delete . If a design error in the program results in this part of the memory not being released, then this part of the space will no longer be able to be used, and a Heap Leak will occur .
- It means that the program uses resources allocated by the system, such as sockets, file descriptors, pipes, etc., without using the corresponding functions to release them, resulting in a waste of system resources, which can seriously lead to reduced system performance and unstable system execution.
3. How to detect memory leaks (understand)
4. How to avoid memory leaks
- 1. Preventive type . Such as smart pointers , etc.
- 2. Check for errors afterwards . Such as leak detection tools .
(3) The use and principle of smart pointers
1、RAII
- No need to explicitly release resources.
- In this way, the resources required by the object remain valid throughout its lifetime
At this time, for the code we gave above, a solution to the SmartPtr class designed using RAII ideas is given:
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _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("除0错误");
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
Output display:
【explain】
To appeal to this approach, the resources applied for at this time are not managed by oneself but handed over to the smart pointer, that is, the smart pointer is used to construct a smart pointer object;
Regarding the three situations of p1, p2 and div system calls mentioned earlier, we will analyze it at this time and see what it looks like in such a scenario?
- According to the output display effect, we can find that they are all normal releases.
- At this time, some young people are curious, why did we release it without deleting it? In fact, this is because sp1 and sp2 are both local objects. When a local object goes out of scope, its destructor will be called.
If sp1 throws an exception:
- If this new throws an exception, it will not enter the constructor, because this new is an actual parameter. The actual parameter will call new first, and new will call operator new and throw an exception. Will it leave directly, so there is no need to release resources? What;
If sp2 throws an exception:
- If the second one throws an exception, speaking of throwing an exception here, it jumps directly to the catch area. In fact, it will end the stack frame first, and the object inside will call the destructor. Calling the destructor will release the resource managed by SP2;
If div() throws an exception:
- If div throws an exception, it will end this function, and these local objects will call the fictitious function, and the release will be completed.
2. Principle of smart pointer
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
- At this point, we can use it normally like a pointer:
- 1. RAII characteristics
- 2. Overload operator* and opertaor-> , which behave like a pointer.
3、std::auto_ptr
auto_ptr documentation introduction
auto_ptr is a smart pointer introduced in the C++98 standard for managing dynamically allocated memory resources. It provides a simple ownership transfer mechanism that allows resource ownership to be transferred from one auto_ptr instance to another.
Here are some important things to know about auto_ptr :
-
Ownership transfer : auto_ptr allows resource ownership to be transferred from one instance to another through an assignment operation. This means that once a resource is assigned to another auto_ptr , the original auto_ptr no longer owns the resource:
int main()
{
auto_ptr<int> ptr1(new int(5));
auto_ptr<int> ptr2;
ptr2 = ptr1; // 所有权转移
//cout << *ptr2 << endl; // 输出 5
cout << *ptr1 << endl; // 错误!ptr1 不再拥有指针,已经转移给了 ptr2
return 0;
}
Output display:
【explain】
- In the above code,
ptr1
we have a dynamically allocatedint
type pointer and assign it toptr2;
- Because of the ownership transfer feature of auto_ptr
ptr1
, the pointer is no longer owned at this time, but the ownership is transferred toptr2
. Therefore, trying to useptr1
dereferencing results in undefined behavior.
-
Dangling pointer problem : auto_ptr has a dangling pointer problem, that is, after the resource ownership is transferred, the original auto_ptr will become a null pointer, but the resource may still be used by another auto_ptr , which may lead to unpredictable behavior.
int main()
{
auto_ptr<int> ptr(new int(5));
if (true)
{
auto_ptr<int> otherPtr = ptr;
//...
}
cout << *ptr << endl; // 输出不确定的值,可能导致程序崩溃
return 0;
}
Output display:
【explain】
- In the above code,
ptr
we have a dynamically allocatedint
type pointer. This pointer is then transferred tootherPtr
, andif
after the statement block ends,otherPtr
it goes out of scope, frees the pointer, and sets it tonullptr;
- At this point,
ptr
it becomes a dangling pointer, and accessing it results in undefined behavior.
【summary】
4、std::unique_ptr
Because of these problems with auto_ptr , it was deprecated in the C++11 standard. In modern C++, it is recommended to use unique_ptr instead of auto_ptr . unique_ptr provides better semantics and security, while supporting move semantics and custom deleters, making resource management more flexible and reliable.
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(unique_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//c++11 思路:语法直接支持
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
//c++98思路:只声明不实现,但是用的人可能会在外面强行定义,所以可以声明为私有
/*private:
unique_ptr(const unique_ptr<T>& up);*/
private:
T* _ptr;
};
void Test_unique()
{
unique_ptr<int> up1(new int);
unique_ptr<int> up2(up1);
}
Output display:
When we use the one that comes with the compiler, let’s see what the effect looks like:
【explain】
- Trying to copy-construct std::unique_ptr will result in a compilation error;
- The problem is that unique_ptr is a smart pointer with exclusive ownership. This means that a unique_ptr can only own one resource and is not allowed to copy it through the regular copy constructor;
- move can be used to move resource ownership from one unique_ptr to another, but direct copy construction is not allowed.
Code modification:
【explain】
- In this revised code, the move function
up1
transfers the resource ownership inup2
to avoid compilation errors. - In summary, the error is caused by the exclusive ownership nature of unique_ptr . It only allows a unique owner of a resource, so a unique_ptr cannot be copied via the regular copy constructor . To transfer resource ownership, use the move function.
5、std::shared_ptr
shared_ptr was introduced to solve the problem of shared ownership in resource management . In many cases, multiple pointers need to own the same resource, and it is necessary to ensure that the resource is not destroyed until the last pointer using it is released. shared_ptr provides a smart pointer implementation that allows multiple pointers to share ownership of a resource while ensuring safe release of the resource.
- 1. Shared_ptr internally maintains a count for each resource to record that the resource is shared .
- 2. When the object is destroyed ( that is, the destructor is called ) , it means that you are no longer using the resource, and the reference count of the object is reduced by one.
- 3. If the reference count is 0 , it means that you are the last object to use the resource and must release the resource ;
- 4. If it is not 0 , it means that other objects besides itself are using the resource, and the resource cannot be released , otherwise other objects will become wild pointers.
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_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)
{
AddRef();
}
void Release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pcount) == 0 && _ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
void AddRef()
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
shared_ptr<T>& operator = (const shared_ptr<T> sp)
{
if (_ptr != sp._ptr) //防止自己给自己赋值
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
int use_count()
{
return *_pcount;
}
~shared_ptr()
{
Release();
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
mutex* _pmtx;
};
Output display:
【explain】
- The above code shows a simplified version of C++11
shared_ptr
implementation. This is a template class that manages dynamically allocated memory and implements shared ownership; - The implementation method is to use reference counting technology, which records the number of shared pointers pointing to the same dynamically allocated memory and releases the memory when the last pointer is destroyed;
- In order to ensure thread safety, a mutex lock is used to synchronize the increase and decrease operations of the counter;
- In addition, in order to make shared pointers can be used like ordinary pointers, overloaded dereference operators and member access operators are also provided.
Through the following program we test the thread safety issue of shared_ptr . It should be noted that the thread safety of shared_ptr is divided into two aspects:
- 1. The reference count in a smart pointer object is shared by multiple smart pointer objects. The reference count of the smart pointer in two threads is ++ or -- at the same time. This operation is not atomic. The reference count was originally 1 and ++ ed twice. , maybe still 2. In this way, the reference count will be messed up. This can cause problems such as resources not being released or the program crashing. Therefore, reference counting ++ and -- in smart pointers need to be locked, which means that reference counting operations are thread-safe.
- 2. The object managed by the smart pointer is stored on the heap, and accessing it in two threads at the same time will lead to thread safety issues.
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
~Date()
{}
};
void SharePtrFunc(zp::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
cout << sp.get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
zp::shared_ptr<Date> copy(sp);
// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n
//次,但是最终看到的结果,并一定是加了2n
{
unique_lock<mutex> lk(mtx);
copy->_year++;
copy->_month++;
copy->_day++;
}
}
}
void test_shared_safe()
{
zp::shared_ptr<Date> p(new Date);
cout << p.get() << endl;
const size_t n = 50000;
mutex mtx;
thread t1(SharePtrFunc, ref(p), n, ref(mtx));
thread t2(SharePtrFunc, ref(p), n, ref(mtx));
t1.join();
t2.join();
cout << p.use_count() << endl;
cout << p->_year << endl;
cout << p->_month << endl;
cout << p->_day << endl;
}
Output display:
- When we are not performing locking operations:
- When we perform a locking operation:
Circular reference of shared_ptr:
- Next, we first give the code for analysis and explanation:
struct ListNode
{
int _data;
zp::shared_ptr<ListNode> _prev;
zp::shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void Test_cycle()
{
zp::shared_ptr<ListNode> node1(new ListNode);
zp::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;
}
Output display:
【explain】
- 1. 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 .
- 2. node1 's _next points to node2 , node2 's _prev points to node1 , and the reference count becomes 2 .
- 3. node1 and node2 are destructed 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. In other words , _next is destructed and node2 is released.
- 5. In other words , _prev is destructed and node1 is released.
- 6. However, _next is a member of node . When node1 is released, _next will be destructed. Node1 is managed by _prev , and _prev is a member of node2 , so this is called a circular reference and no one will release it.
Due to the problem of circular references, when the program exits Test_cycle(), the reference counts of the two nodes will not drop to 0, so their destructors will not be called. This means that the output statements in the destructor will not be executed, potentially leading to the risk of memory leaks.
In order to avoid circular reference problems, you can set the _prev and _next member variables as weak_ptr, which is a weak reference to shared_ptr and does not increase the reference count. In this way, in the circular linked list, weak_ptr is used to break the strong reference relationship and prevent memory leaks caused by circular references.
- Here is sample code to fix the circular reference problem:
First, we first implement a weak_ptr manually or use the weak_ptr provided in the library. Here, I implemented one manually:
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
};
The corrected code is as follows:
struct ListNode
{
int _data;
zp::weak_ptr<ListNode> _prev;
zp::weak_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void Test_cycle()
{
zp::shared_ptr<ListNode> node1(new ListNode);
zp::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;
}
Output display:
【explain】
- In the repaired code, change the member variables _prev and _next in the ListNode structure to the zp::weak_ptr type, so that a strong reference to the next node will not be added and the circular reference problem will be avoided;
- When the program exits the Test_cycle() function, the reference counts of node1 and node2 will drop to 0, the destructor
~ListNode()
will be called, and the related memory resources will be released correctly, avoiding the problem of memory leaks.
6、weak_ptr
In the above shared_ptr description, we used weak_ptr. Next, we will formally introduce it.
basic introduction:
- weak_ptr is a smart pointer that does not control the life cycle of the object . It points to an object managed by shared_ptr ;
- The object's memory management is performed by the strongly referenced shared_ptr . weak_ptr only provides a means of access to managed objects;
- The purpose of weak_ptr design is to introduce a smart pointer to cooperate with shared_ptr to assist shared_ptr work. It can only be constructed from a shared_ptr or another weak_ptr object . Its construction and destruction will not cause the reference count to increase or decrease ;
- weak_ptr is used to solve the deadlock problem when shared_ptr refers to each other. If two shared_ptr refers to , then the reference count of the two pointers can never drop to 0, and the resource will never be released;
- It is a weak reference to the object and does not increase the reference count of the object. It can be converted to and from shared_ptr . shared_ptr can be directly assigned to it. It can obtain shared_ptr by calling the lock function.
Next, let's take a brief look at the code:
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout << "A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout << "B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl;
cout << pa.use_count() << endl;
}
int main()
{
fun();
return 0;
}
Output display:
【explain】
- You can see that in the fun function , pa and pb refer to each other. The reference counts of the two resources are 2. When the function is to be jumped out, the reference counts of the two resources will be reduced by one when the smart pointers pa and pb are destructed;
- However, the reference count of both is still 1 , resulting in resources not being released when jumping out of the function ( the destructor of AB is not called). If one of them is changed to weak_ptr, it will be fine. We change the shared_ptr pb_ in class A ;
- In this case, the reference of resource B is only 1 at the beginning . When pb is destructed, B 's count becomes 0 , and B is released. When B is released, A 's count will also be reduced by one. At the same time, when pa is destructed, A 's count will be reduced. The count is decremented by one, then A 's count is 0 and A is released.
Note : We cannot directly access the object's method through weak_ptr , we should convert it to shared_ptr first! ! !
7. Deleteer
If the object is not new, how can it be managed through smart pointers? In fact, shared_ptr designed a deleter to solve this problem! !
1. Definition
- Smart pointer deleter refers to a custom operation performed when a smart pointer is destructed;
- The deleter can perform additional cleanup work or custom logic when releasing the resources managed by the smart pointer.
- Here's an example of using a Lambda expression as a deleter:
int main()
{
int* p = new int(10);
std::shared_ptr<int> sp(
p,
[](int* ptr)
{
std::cout << "deleting pointer " << ptr << std::endl; delete ptr;
});
// 输出共享指针的引用计数
std::cout << "sp use_count: " << sp.use_count() << std::endl;
// 手动将共享指针的引用计数减 1
sp.reset();
return 0;
}
Output display:
【explain】
- The above code creates a
p
shared pointer pointing to an integer variable and uses a deleter Lambda expression to output the address of the deleted object and release the memory allocated on the heap; - After
sp.reset()
the call, the reference count becomes zero and the deleter is called to free the memory.
(4) The relationship between smart pointers in C++11 and boost
First, C++11 introduced smart pointers in the standard library, including std::shared_ptr
and std::unique_ptr
. These smart pointers provide a mechanism for managing resource ownership, which can automatically perform memory management and avoid the trouble of manually releasing resources. C++11's smart pointers are implemented by introducing new language features and library support.
Boost is a popular C++ extension library that provides a large collection of high-quality, widely tested and used C++ code. Before the C++11 standard introduced smart pointers, Boost already provided its own smart pointer library, including boost::shared_ptr
and boost::scoped_ptr
etc. These smart pointers are widely used and highly praised in the C++ community.
In fact, the smart pointers in the C++11 standard library are influenced and inspired by Boost smart pointers. std::shared_ptr
The design and functionality of sum in the C++11 standard are basically quite similar to sum std::unique_ptr
in Boost . C++11 smart pointers also introduce some new features and improvements, such as move semantics and custom deleters, to provide better performance and flexibility.boost::shared_ptr
boost::scoped_ptr
Summarize
At this point, the explanation about smart pointers is all over. Next, briefly review and summarize this article! ! !
A smart pointer is a pointer used to automatically manage dynamically allocated resources. It provides automated memory management that can reduce common resource management problems such as memory leaks and dangling pointers.
Common smart pointer types:
std::shared_ptr
: Allows multiple pointers to share the same memory resource and uses reference counting for memory management.std::unique_ptr
: Exclusive pointer, which guarantees that only one pointer can access the resource, has move semantics, and can be used to realize the transfer of ownership.std::weak_ptr
: Weak reference pointer, used to solvestd::shared_ptr
the problem of resource leaks caused by circular references.
Advantages of smart pointers:
- Automatically release resources : Smart pointers automatically release managed resources through destructors, avoiding the cumbersome process of manually releasing resources.
- Avoid memory leaks : Smart pointers use reference counting or exclusive ownership to ensure that resources are released correctly when they are no longer used, thus avoiding memory leaks.
- Improve security: Smart pointers can reduce the problems of dangling pointers and wild pointers, and improve the security and stability of the program.
The above is the entire content of this article. Thank you everyone for watching and supporting! ! !