C + + smart pointers
Memory management is a common source of errors and bugs in C + +. In most cases, these bugs come from the use of dynamically allocated memory and pointers: when dynamically allocated memory is released multiple times, memory corruption or fatal run-time errors can result, and memory leaks occur when you forget to release dynamically allocated memory. So we need a smart pointer to help us manage the dynamically allocated memory. It comes from the fact that stacks are more secure than heaps, because variables on the stack are automatically destroyed and cleaned up after they leave the scope. Smart pointers combine the security of variables on the stack with the flexibility of the variables on the heap. Introduction
Consider one of the following functions:
void SomeFunction ()
{
resource* ptr = new Resource;//Resource is a class or struct
//using PTR processing
//...
Delete ptr;
}
The code is simple: apply for a dynamic memory and release it after use. But we can easily release it before the end of the function. Maybe we remember to release the dynamically requested memory in time, but there are still some force majeure causing the memory not to be released, such as the function prematurely terminated. Consider the following code:
void SomeFunction ()
{
resource* ptr = new Resource;//Resource is a class or struct
int x;
Std::cout << "Enter an integer:";
Std::cin >> x;
if (x = = 0) return
; function terminated, unable to free PTR
if (x < 0)
throw; Exception, function terminated, could not release PTR
//Use PTR processing
//...
Delete ptr;
}
At this point, because of the premature return statement and the exception throw, PTR will not be properly released, resulting in a memory leak. In the final analysis, pointers do not have an intrinsic mechanism to automate management and release. Then you might think of the class: the class stores the pointer internally, and then destroys the pointer in the destructor. Class enables automatic management of resources. The advantage is that, as long as the class local variable (allocated on the stack) exceeds its scope (regardless of how it is left), its destructor is bound to be executed, and the managed memory will be destroyed. Based on this idea, we have implemented a simple smart pointer class:
Template<typename t>
class auto_ptr1
{public
:
auto_ptr1 (t* ptr = nullptr):
m_ptr{ptr}
{}
Virtual ~auto_ptr1 ()
{
delete m_ptr;
}
t& operator* () {return *m_ptr;}
t* operator-> () {return m_ptr;}
Private:
t* m_ptr;
Class Resource
{public
:
Resource () {cout << "Resource acquired!" << Endl;}
Virtual ~resource () {cout << "Resource destoryed!" << Endl;}
;
int main ()
{
{
auto_ptr1<resource> res (new Resource);
}
Cin.ignore (ten);
return 0;
}
To execute the above program, we can get the following output:
Resource acquired
Resource destroyed
It seems that this idea can be completely, we will dynamically request resources to a class variable to save, because the class variable in the local scope, it will automatically call the destructor, and then free memory. At the same time, regardless of how it leaves the scope, even if an exception occurs, the destructor is bound to be executed, and the memory must be released because the class variable is stored on the stack.
However, the above implementation has a fatal hidden danger, consider the following code:
int main ()
{
{
auto_ptr1<resource> res1 (new Resource);
Auto_ptr1<resource> Res2 (RES1);
}
Cin.ignore (ten);
return 0;
}
It doesn't seem to be a problem, but when it does, the program crashes. Because the res2 is initialized with RES1, the default copy constructor is invoked, and a shallow copy is performed. Therefore, Res2 and res1 internal storage is the same memory, when the destruction of variables, the same memory will be released multiple times, the program will run. Similarly, the following code still has the same problem:
void Passbyvalue (auto_ptr1<resource> res)
{}
int main ()
{
{
Auto_ptr1<resource > res1 (New Resource);
Passbyvalue (RES1);
}
Cin.ignore (ten);
return 0;
}
Because the res1 is shallow copied into the function parameter res, its memory is released after the function is executed, and when the res1 is destroyed, the freed memory is destroyed, and the program crashes.
So we need to modify this class, and it's best to implement the copy constructor ourselves, as well as to implement the assignment operator overload ourselves. If we move the pointer ownership from one object to another in both functions, the above problem will be solved. The modified smart pointer class is as follows:
Template<typename t>
class auto_ptr2
{public
:
auto_ptr2 (t* ptr = nullptr):
m_ptr{ptr}
{}
Virtual ~auto_ptr2 ()
{
delete m_ptr;
}
AUTO_PTR2 (auto_ptr2& rhs)
{
m_ptr = rhs.m_ptr;
Rhs.m_ptr = nullptr;
}
auto_ptr2& operator= (auto_ptr2& rhs)
{
if (&RHS = this) return
*this;
Delete m_ptr;
M_ptr = rhs.m_ptr;
Rhs.m_ptr = nullptr;
return *this;
}
t& operator* () {return *m_ptr;}
t* operator-> () {return m_ptr;}
BOOL IsNull () const {return m_ptr = = nullptr;}
Private:
t* m_ptr;
We use this new class to test the following code:
int main ()
{
auto_ptr2<resource> res1 (new Resource);
Auto_ptr2<resource> Res2; Initialized to nullptr
cout << "Res1 is" << (Res1.isnull ()?) Null\n ":" Not null\n ");
cout << "Res2 is" << (Res2.isnull ()?) Null\n ":" Not null\n ");
Res2 = res1; Transfer pointer ownership
cout << "ownership transferred\n";
cout << "Res1 is" << (Res1.isnull ()?) Null\n ":" Not null\n ");
cout << "Res2 is" << (Res2.isnull ()?) Null\n ":" Not null\n ");
Cin.ignore (ten);
return 0;
}
The program output is as follows:
Resource acquired
res1 is not null
res2 are null
ownership transferred
res1 is null
res2 are not nu ll
Resource destroyed
As you can see, the above code does not make an error because the overloaded assignment operator implements a pointer ownership transfer.
If you look closely at AUTO_PTR2, you will find that it actually implements the mobile semantics, which, for mobile semantics, will transfer object ownership rather than assignment. There is no mechanism to implement the move semantics because there is no right value reference before c++11. So the smart pointer before c++11 is STD::AUTO_PTR, and its implementation is similar to the AUTO_PTR2 class. But it has a lot of problems. First, if there is a std::auto_ptr type of argument in the function, and you use a variable to pass the value, the resource ownership will be shifted, then the resource will be destroyed after the function is finished, and then you may be able to dereference the variable, but it is already a null pointer, so the program may crash. Second, the std::auto_ptr delete is called internally, so the std::auto_ptr does not work correctly for dynamically allocated arrays, and memory leaks may occur. Finally, the STD::AUTO_PTR is incompatible with STL, because the object of STL is copying, not moving semantics. So in fact, the std::auto_ptr has been deprecated in c++11, and the standard library has been removed from the c++17.
Based on the right value reference and mobile semantics in c++11, we can solve most of the problems that arise:
template<typename t> class Auto_ptr3 {public:auto_ptr3 (t* ptr = nullptr): m_p
TR{PTR} {} auto_ptr3 (const auto_ptr3& RHS) = delete;
AUTO_PTR3 (auto_ptr3&& RHS): m_ptr{rhs.m_ptr} {rhs.m_ptr = nullptr;
} auto_ptr3& operator= (const auto_ptr3& RHS) = delete;
auto_ptr3& operator= (auto_ptr3&& rhs) {if (this = = &RHS) {return *this;
} std::swap (M_ptr, rhs.m_ptr);
return *this;
Virtual ~auto_ptr3 () {delete m_ptr;
} t& operator* () {return *m_ptr;}
t* operator-> () {return m_ptr;}
BOOL IsNull () const {return m_ptr = = nullptr;} private:t* m_ptr; };
You can see that AUTO_PTR3 implements the overload of the move constructor and the mobile assignment operator, and then implements the mobile semantics, but also disables the copy constructor and the copy assignment operator, so the variables of this class can only pass the right value, but not the left value. But you can pass the right value to the const left value reference parameter of the function. When you pass the right value, then obviously you already know that you want to transfer the pointer ownership, then the current variable will no longer be valid. There is a similar realization in c++11, that is std::unique_ptr, of course, more intelligent.
The C++11 standard library contains four kinds of smart pointers: std::auto_ptr (do not use), Std::unique_ptr, Std::shared_ptr, and Std::weak_ptr. Let's introduce the next three smart pointers individually. std::unique_ptr
Std::unique_ptr is a std::auto_ptr substitute for memory management that cannot be shared by multiple instances. This means that only one instance has memory ownership. It's very simple to use:
Class Fraction {Private:int m_numerator = 0;
int m_denominator = 1;
public:fraction (int numerator = 0, int denominator = 1): M_numerator (numerator), m_denominator (denominator) {} friend std::ostream& operator<< (std::ostream& out, const fraction &f1) {OU
T << f1.m_numerator << "/" << f1.m_denominator;
return out;
}
};
int main () {std::unique_ptr<fraction> f1{new fraction{3, 5}}; cout << *f1 << Endl; OUTPUT:3/5 std::unique_ptr<fraction> F2; Initialized to nullptr//F2 = f1//Illegal, the left value assignment is not allowed F2 = Std::move (F1);
At this point F1 shifts to f2,f1 to nullptr//c++14 can use the Make_unique function auto F3 = std::make_unique<fraction> (2, 7); cout << *f3 << Endl;
OUTPUT:2/7//process array, but try not to do this because you can use Std::array or std::vector auto F4 = std::make_unique<fraction[]> (4); Std::cout << f4[0] << Endl; OUTPUT:0/1 cin.iGnore (10);
return 0; }
If the compiler supports, try to create a unique_ptr instance using the Make_unique function, and if not, you can implement a simplified version:
Note: Unable to process array
template<typename T, TypeName ... Ts>
std::unique_ptr<t> make_unique (Ts ... args) {return
std::unique_ptr<t> {new t{std:: Forward<ts> (args) ...}}
As you can see, the Std::unique_ptr object can be used as a function return value because the function return value is a right value and is replicated to other variables by moving semantics. Of course, you can pass the Std::unique_ptr object to the function, and look at the following example:
Class Resource
{public
:
Resource () {cout << "Resource acquired!" << Endl;}
Virtual ~resource () {cout << "Resource destoryed!" << Endl;}
Friend std::ostream& operator<< (std::ostream& out, const Resource &res)
{out
<< "I am A" Resource "<< Endl;
return out;
}
;
void Useresource (const std::unique_ptr<resource>& res)
{
if (res)
{
cout << *res ;
}
}
int main ()
{
{
auto ptr = std::make_unique<resource> ();
Useresource (PTR);
cout << "Ending" << Endl;
}
Output
//Resource acquired
//I am a Resource
//Ending
//Resource destroyed Cin.ignore
(10 );
return 0;
}
You can see that the Std::unique_ptr object can pass values to the left value constant reference parameter because it does not change memory ownership. You can also transfer values to the right value to implement the move semantics:
void TakeOwnership (std::unique_ptr<resource>&& res)//can also be used std::unique_ptr<resource> res
{
if (res)
{
cout << *res;
}
}
int main ()
{
{
auto ptr = std::make_unique<resource> ();
TakeOwnership (PTR); Illegal
takeownership (Std::move (PTR));//must pass right value
cout << "ending" << Endl;
}
Output
//Resource acquired
//I am a Resource
//Resource destroyed
//ending
Cin.ignore (10 );
return 0;
}
As you can see, the Std::unique_ptr object makes it easy to manage dynamic memory. But the premise is that the object is built on the stack, do not use dynamically allocated class objects, then on the heap, its behavior and normal pointers become the same.
The two errors that can be made using Std::unique_ptr are:
Never use the same resource to initialize multiple Std::unique_ptr objects
Resource *res = new Resource;
Std::unique_ptr<resource> res1 (res);
Std::unique_ptr<resource> Res2 (res);
Do not mix common pointers with smart pointer
Resource *res = new Resource;
Std::unique_ptr<resource> res1 (res);
Delete res;
Std::unique_ptr uses the new and delete operators to allocate and release memory by default, and you can modify this behavior, and the following code uses the malloc () and free () functions to manage resources:
Most of the time there's no reason to do it.
auto deleter = [] (int* p) {free (P);};
int* p = (int*) malloc (sizeof (int));
*p = 2;
Std::unique_ptr<int, Decltype (deleter) > mysmartptr{p, deleter};
cout << *mysmartptr << Endl; Output:2
Std::unique_ptr also has several common methods:
1. Release (): Returns the pointer managed by the object while releasing its ownership;
2. Reset (): To deconstruct the memory of its management, but also to pass in a new pointer object;
3. Swap (): An object managed by a clearing house;
4. Get (): Returns the pointer managed by the object;
5. Get_deleter (): Returns a Call function that reconstructs its management pointer.
Use the above method with caution, such as not to pass the pointer managed by another object to the reset () method of another object, which causes a chunk of memory to be released more than once. More details can be consulted here. std::shared_ptr
The std::shared_ptr is similar to std::unique_ptr. To create a Std::shared_ptr object, you can use the make_shared () function (c++11 is supported, and people who seem to be making this standard forget Make_unique (), so append in c++14). The main difference between std::shared_ptr and Std::unique_ptr is that the former is a smart pointer that uses reference counting. Reference-counted smart pointers can track the number of smart pointer instances that reference the same real-world pointer object. This means that there can be multiple std::shared_ptr instances that can point to the same dynamically allocated memory, which is released when the last referenced object leaves its scope. Another difference is that std::shared_ptr cannot be used to manage dynamic arrays of the C language style. Here's a look at the example:
int main ()
{
Auto ptr1 = std::make_shared<resource> ();
cout << ptr1.use_count () << Endl; Output:1
{
auto ptr2 = ptr1; Two objects are managed with the same piece of memory std::shared_ptr<resource> PTR3 by the copy constructor
; Initialized to empty
PTR3 = ptr1; By assigning value, shared memory
cout << ptr1.use_count () << Endl; Output:3
cout << ptr2.use_count () << Endl; Output:3
cout << ptr3.use_count () << Endl; Output:3
}
//At this time Ptr2 and Ptr3 object were reconstructed
cout << ptr1.use_count () << Endl; Output:1
Cin.ignore (a);
return 0;
}
As you can see, it's important to share the memory by copying the constructor or assigning values, see the following example:
int main ()
{
resource* res = new Resource;
Std::shared_ptr<resource> ptr1{res};
cout << ptr1.use_count () << Endl; Output:1
{
std::shared_ptr<resource> ptr2{res}; Initializes
cout << ptr1.use_count () << Endl with the same block of memory; Output:1
cout << ptr2.use_count () << Endl; Output:1
}
//Ptr2ptr3 object destructor at this time, Output:resource destroyed
cout << ptr1.use_count () << Endl; Output:1
Cin.ignore (a);
return 0;
}
Oddly, PTR1 and PTR2 are initialized with the same block of memory, but this share is not known to two objects. This is because two objects are initialized independently, and they do not communicate with each other. Of course, the above program will eventually crash, because the same piece of memory will be destructor two times. So, there is also the use of copy constructors and assignment operations to make different objects manage the same piece of memory. If deep digging, std::shared_ptr and std::unique_ptr internal implementation mechanism is different, the former using two pointers, a pointer to manage the actual pointer, the other pointer to a "control block", which records which objects jointly manage the same pointer. This is done in initialization, so if two objects are initialized individually, their respective "control blocks" are not recorded with each other, although the same memory is managed. So, the above problem arises. However, if you use the copy constructor and the assignment operation, the control block synchronizes the update so that the reference count is reached. Use std::make_shared will not appear above the problem, so recommend to use.
Std::shared_ptr There are other ways to get more information here. std::weak_ptr
Std::shared_ptr can implement multiple objects to share the same block of memory, and when the last object leaves its scope, the memory is freed. However, there is still the possibility of memory can not be released, reminiscent of the "deadlock" phenomenon, for std::shared_ptr will appear a similar "circular reference" phenomenon:
class person {public:person (const string& name): m_name{name} {cou
T << m_name << "created" << Endl;
Virtual ~person () {cout << m_name << "destoryed" << Endl; friend bool Partnerup (std::shared_ptr<person>& p1, std::shared_ptr<person>& p2) {i
F (!p1 | |!p2) {return false;
} P1->m_partner = p2;
P2->m_partner = p1;
cout << p1->m_name << "is now partenered with" << p2->m_name << Endl;
return true;
} private:string M_name;
Std::shared_ptr<person> M_partner;
};
int main () {{Auto P1 = std::make_shared<person> ("Lucy");
Auto P2 = std::make_shared<person> ("Ricky"); Partnerup (P1, p2);
Set each other as partners} Cin.ignore (10);
return 0; }
The entire program is simple, creating two person dynamic objects, referred to the smart pointer management, and referencing each other through the Partnerup () function as their partner. But the results of the execution are problematic:
Lucy created
Ricky created
Lucy is now partnered with Ricky
object is not destructor. A memory leak occurred. Think carefully about when the Std::shared_ptr object is to be destructor, that is, the reference count becomes 0 o'clock, but when you want to deconstruct the P1, the P2 references P1, cannot be refactored, and vice versa. Mutual references cause a "deadlock" and eventually memory leaks. This situation also appears in the self-locking:
int main ()
{
{
auto P1 = std::make_shared<person> ("Lucy");
Partnerup (P1, p1); Himself as his partner
}
Cin.ignore (a);
return 0;
}
At this time Std::weak_ptr emerged. Std::weak_ptr can contain references to memory that is managed by std::shared_ptr. But it is merely a spectator, not an owner. That is, Std::weak_ptr does not own this memory, and of course it does not count, nor does it prevent std::shared_ptr from releasing its memory. However, it can access this block of memory by returning a Std::shared_ptr object through the Lock () method. So we can use std::weak_ptr to solve the "circular reference" problem above:
class person {public:person (const string& name): m_name{name} {cou
T << m_name << "created" << Endl;
Virtual ~person () {cout << m_name << "destoryed" << Endl; friend bool Partnerup (std::shared_ptr<person>& p1, std::shared_ptr<person>& p2) {i
F (!p1 | |!p2) {return false; } P1->m_partner = p2;
The shared_ptr object P2->m_partner = P1 can be received in the weak_ptr overloaded assignment operator;
cout << p1->m_name << "is now partenered with" << p2->m_name << Endl;
return true;
} private:string M_name;
Std::weak_ptr<person> M_partner;
};
int main () {{Auto P1 = std::make_shared<person> ("Lucy");
Auto P2 = std::make_shared<person> ("Ricky"); Partnerup (P1, p2);
Set each other as partners} Cin.ignore (10);
return 0; }
Program Normal output (note that the order of creation and destructor is reversed.) On the stack. ):
Lucy created
Ricky created
Lucy's now partnered with Ricky Ricky
destroyed
Lucy destroyed
More information about Std::weak_ptr is here.
Finally say the feeling: still want to deeply understand the internal mechanism of the smart pointer, mining can use it, otherwise you may encounter the same problem as the normal pointer. References
[1] CPP leraning Online (especially thanks for this tutorial, most of the information in this article can be seen here).
[2] Marc Gregoire. Professional C + +, third Edition, 2016.
[3] cppreference Dynamic memory management
[4] Lvor Horton. Using the C + + Standard Template libraries, 2016.
[5] Ochangkun (Orongzi), high-speed hands-on C + + 11/14.