Understanding and basic usage summary of smart pointer in C + +

1 overview of smart pointer

There is no doubt that the smart pointer is more intelligent than the ordinary bare pointer (that is, the pointer of the object we directly use new). The most clear embodiment is that it can automatically help you manage the problem of memory leakage. In other words, using the smart pointer does not require you to delete a pointer manually;
Simply put: a pointer is a layer of packaging for an ordinary bare pointer. After packaging, it makes the pointer more intelligent and can automatically release memory for you at the right time;

The C + + standard library provides the use of four smart pointers:
std::auto_ptr; c++98 has a smart pointer, but now it is abandoned and completely STD:: unique_ Replaced by PTR;
The following three are the new smart pointers provided by C++11;
std::unique_ptr; An exclusive smart pointer. Only one pointer can point to the object at a time;
std::shared_ptr; Multiple pointers can point to the pointer of the same object;
std::weak_ptr; An auxiliary std::shared_ptr pointer exists;

When using smart pointers, remember to include header files #include < memory >

2 shared_ptr basic understanding

shared_ptr refers to the method of sharing ownership to manage the life cycle of the pointed object; In other words, an object can not only be shared by a separate_ PTR can also be pointed to by multiple shared_ptr points to multiple shared_ptr cooperates with each other to jointly manage the life cycle of the pointed object. When the pointed object is not needed, it will release its memory.

And we use shared_ Before PTR: you need to think about whether the pointed object needs to be pointed by multiple pointers, that is, multiple pointers can share an object (multiple pointers point to the same memory);

shared_ The principle of PTR memory management is to use reference counting. This way: you can use shared in the last smart pointer pointing to an object_ When PTR no longer needs to point to the memory, it releases the space of the object;

Let's think about a problem: the last shared that points to the object's memory_ When will the PTR pointer destroy the memory space of the object
1. The shared_ When the PTR pointer is destructed, that is, when the life cycle of the pointer ends;
2. This shared_ When PTR points to other objects;

3 shared_ Initialization mode of PTR

We should know that a smart pointer is a template. In essence, a smart pointer is an object. It is called a pointer because the - > operator is overloaded in the smart pointer class, so that the object can point to the memory it needs like a pointer;

3.1 default initialization

Therefore, the initialization method of smart pointer is similar to that of container: the basic form is as follows
shared_ PTR < type pointed to > smart pointer name;
For example:

shared_ptr<string> p; //Point to a smart pointer p1 of type string,

//This method is default initialization, that is, the smart pointer will be assigned nullptr;

3.2 cooperate with new initialization

//Point to a smart pointer of type int, and the initial value pointing to the object is 10;
shared_ptr<int> p1(new int(10)); 
//Point to a smart pointer of type int, and the initial value pointing to the object is 0;
shared_ptr<int> p2(new int());

We need to know that the initialization mode of the pointed object is determined by the object, that is, the type after new plus the mode in ().

3.3 shared_ptr error usage

shared_ptr<int> p = new int(10); //This way makes it wrong because shared_ptr does not support implicit construction;
//That is to say: the type of int * is on the right of the = sign, and the type of shared is on the left of the = sign_ PTR type, if the type does not correspond, it cannot be used;

The smart pointer is also used as the return value: the following methods also do not work

shared_ptr<int> fun(int x)
{
	return new int(x); //This method is also wrong because it does not support implicit construction, that is, the types do not correspond
}

At first, we can use ordinary bare pointers to initialize smart pointers, but this method is not recommended. It is best not to use:

int* p = new int();
shared_ptr<int> p1(p);

The above method is correct, but it is not recommended

3.4 using std:: make_shared function to initialize

In fact, we can use a special function template make_ The shared function initializes the shard_ptr pointer initialization is also considered the safest and most efficient allocation method (ps: Although I don't know where efficiency is, that's what the reading materials say)

make_ The return value of shared refers to the shared of the object_ PTR pointer.

//Similar: shared_ ptr<int> p (new int(10));
shared_ptr<int> p = std::make_shared<int>(100);

//Similar to shared PTR < string > P (new string (5, 'a');
shared_ptr<string> p = std::make_shared<string>(5,'a');

//It can also be used in combination with auto
auto p = std::make_shared<string>(5,'a'); //This kind of writing is relatively concise

make_ The parameter of the shared function is a parameter according to the initialization method of the pointing object.

However, using make_shared function to initialize, shared_ptr pointer, then share_ PTR pointer can't customize the delegator. (as for what is a deletion device, we will talk about it later);

4 shared_ Increase and decrease of PTR reference count

We know shared_ptr manages the release of memory space pointing to objects through reference counting.
So as long as shared_ When the reference count of PTR is 0, the memory space pointing to the object will be released automatically;

//At this time, the reference count to the shared pointer p1 of type int is 1
shared_ptr<int> p1 = std::make_shared<int>(10); 
//At this time, the reference count of shared pointer p2 pointing to int type is 2, and the reference count of p1 is also 2
auto p2(p1);

It can be understood as follows: each shared object that points to the same object memory_ PTRs are associated with a reference count when there is a new shared_ When PTR points to the memory space of the same memory object, all shared objects pointing to the same memory object_ PTR will increase 1 reference count;
The same is true of reduction;

shared_ptr is understood as the reference count of formal parameters. At this time, the formal parameters are not referenced

void fun(shared_ptr<int> p ) 
{
	//Code logic
	//...
}

int main()
{
	shared_ptr<int> p1(new int(10)); //Reference count has one
	fun(p1); //When the parameter is passed to the formal parameter p of the fun function, when the formal parameter is not destroyed inside the fun function,
			//This shared_ The reference count of PTR is 2. When p comes out of the scope,
			//Then the reference count will be reduced by 1, that is, the formal parameter p is the end of the object life cycle, which is equivalent to that the reference count has not changed
	return 0;
}

Therefore, generally speaking, the formal parameter is the way of assignment. If the actual parameter is received, the reference count is now + 1 and then minus 1, which is equivalent to no change;

Then it's easy to understand that another situation is to share_ PTR receives arguments by reference, and the reference count is actually unchanged;

void fun(shared_ptr<int>& p ) 
{
	//Code logic
	//...
}

int main()
{
	shared_ptr<int> p1(new int(10)); //Reference count has one
	fun(p1); //When the parameter is passed to the formal parameter p of the fun function, the reference count does not change because it is in the way of reference. It is still 1,		
	return 0;
}

There is another way to return a value: shared_ptr is the return value of the function and receives the return value in the form of value:
There are two kinds of situations:
First: when the function is called, if there is no variable to accept the return value, the reference count remains unchanged; In fact, this is not changed, because when returning, it is equivalent to assigning a copy of shared_ptr object, then this assignment is the past share_ptr will point to the memory of the same object. At this time, the reference count will be + 1. However, since no variable receives the return value, the reference count will be reduced by 1, so it is equivalent to no change;
Second: when calling this function, if there is a variable to receive the return value, the reference count + 1;

//First case:
shared_ptr<int> fun(shared_ptr<int>& p ) 
{
	//Code logic
	//...
	return p;
}

int main()
{
	shared_ptr<int> p1(new int(10)); //Reference count has one
	fun(p1); //No one receives the return value of the fun function. The reference count is still 1,		
	return 0;
}
//Case 2:
shared_ptr<int> fun(shared_ptr<int>& p ) 
{
	//Code logic
	//...
	return p;
}

int main()
{
	shared_ptr<int> p1(new int(10)); //Reference count has one
	auto p2 = fun(p1); //Someone received, the reference count is still 1,		
	return 0;
}

Decrease in reference count:
First: when pointing to the shared of the same object_ When PTR points to another object space, the reference count decreases

shared_ptr<int> p1(new int(10)); //Reference count is 1
auto p2(p1); //The reference count is 2, and p1 and p2 are both 2

p2 = std::make_shared<int>(20); //At this time, the reference count of p2 is 1 because it points to a new space,
								//The reference count of p1 is 1 because p2 points to a new object space;

Second: when shared_ptr pointer, after leaving the scope, invokes its destructor. At this time, the reference count will also decrease.

shared_ptr<int> p1(new int(10)); //Reference count is 1
auto p2(p1); //The reference count is 2, and p1 and p2 are both 2

void fun()
{
	shared_ptr p3(p1); //The reference count is 3
	shared_ptr p4(p2); //The reference count is 4
}
//When P3 and P4 leave the scope, the reference count changes to 2 again

5 shared_ Common member functions of PTR

5.1 use_count member function

use_ The count member function is used to count the number of shared_ptr pointer points to the same memory space object;

shared_ptr<int> p1(new int(10));
int nums = p1.use_count(); //nums = 1. There is a reference count at this time

shared_ptr<int> p2(p1);
int nums = p2.use_count(); //nums = 2. There is a reference count at this time

shared_ptr<int> p3(p2);
int nums = p3.use_count(); //nums = 3. There is a reference count at this time

There is one detail: shared_ptr object, which call use_ The count function is OK, and P1, P2 and P3 can call use count

5.2 unique member function

This member function is mainly used to judge: shared_ Whether the PTR pointer has only one smart pointer pointing to the object. If yes, it returns true; if not, it returns false;

shared_ptr<int> p1(new int(10));

if(p1.unique()) //The condition holds because there is only one reference count
{
	//Output this result
	cout<<"only one shared_ptr Pointer to the same memory space"<<endl;
}else
{
	cout<<"Multiple shared_ptr Point to the same memory space"<<endl;
}

shared_ptr<int> p2(p1);

if(p1.unique()) //The condition does not hold at this time because there are only 2 reference counts
{
	cout<<"only one shared_ptr Pointer to the same memory space"<<endl;
}else //Output this result
{
	cout<<"Multiple shared_ptr Point to the same memory space"<<endl;
}

5.3 reset member function

The reset member function resets shared_ptr pointer.

The reset member has two overloaded versions: the first version without parameters: reset the shared_ptr is null, and the reference count is reduced by one. If it is reduced to 0, the memory space pointed to by the pointer will be released;
The second version with parameters: reset shared_ptr points to the memory space object with this parameter, and the reference count of the original memory space object is reduced by one. If it is reduced to 0, the memory space is released;

reset function without parameters

shared_ptr<int> p1(new int(10)); //Reference count is 1
p1.reset(); //p1 points to null, because the reference count will be reduced by one, and then it will become 0, which frees up the memory
shared_ptr<int> p1(new int(10)); //Reference count is 1
shared_ptr p2(p1); //The reference count is 2
p1.reset(); //p1 points to null. Because the reference count will be reduced by one, it will become 1. The reference count of P2 is 1, and the memory space pointed to by P2 is not released

Version reset function with parameters

shared_ptr<int> p1(new int(10)); //Reference count is 1
p1.reset(new int(20)); //p1 points to the newly opened object memory space. Since the reference count will be reduced by one,
						//Therefore, the memory space of the object pointed to by P1 is released
shared_ptr<int> p1(new int(10)); //Reference count is 1
shared_ptr p2(p1); //The reference count is 2
p1.reset(new int(20)); //p1 points to the newly opened object memory space. Since the reference count will be reduced by one,
						//After subtraction, it becomes 1, the reference count of P2 is 1, and the memory space pointed to by P2 is not released

6. The problem of specifying the remover and pointing to the array

The second parameter of smart pointer initialization in C + + can specify a custom remover. In fact, this remover is a function pointer and a single parameter function pointer. Of course, you can also pass a lambda expression.
If the parameter of the second initialization is not specified, the default Deleter is used, that is, the version of direct delete;

Why specify your own delegator?
Because the smart pointer needs to release the memory of the array when managing the array pointer. If you use the default deletion device, that is, delete directly, it will lead to memory leakage. Therefore, you need to specify your own deletion device to release the array memory;

class A
{
public:
	A()
	{
		cout<<"A()Constructor Execution "<<endl;
	}
	~A()
	{
		cout<<"A()Destructor execution"<<endl;
	}
};

int main()
{
	//shared_ ptr<A> P(new A[10]); // Try to open up 10 class a array spaces and point to it with smart pointer P;
								//However, an error will be reported. The reason for the error is the delete p used by the default Deleter,
								//In this way, only one array element can be destructed, and the remaining 9 are not destructed successfully
								//What we need is to release memory in the way of delete[]p, so we need to specify the deletion device ourselves
	
	shared_ptr<A> p(new A[10],[](A* p){
	 delete[] p;}); //Specifying a delegator with a lambda expression
	 				//This frees up the memory of the clean array
	 				
	//In fact, there is also a class template STD:: default provided by the C + + standard library_ delete
	//This method can also be used to delete arrays
	shared_ptr<A> p2(new A[10], std::default_delete<A[]>());
	return 0;
}

C++17 provides a more convenient way to manage arrays, but this is not supported in C++11 and 14, so the old compiler may report errors

Only add [] brackets to the types in < > angle brackets and () parentheses.

shared_ptr<A[]> p(new A[10]); //c++17 began to support this writing method to manage arrays

7 shared_ Circular reference problem caused by PTR – weak_ptr solution

What is circular reference? What is weak_ptr;

What is weak_ptr pointer
1. First of all, we need to know weak_ptr: an auxiliary shared_ Intelligent pointer of PTR;
That is to say, weak_ptr itself cannot be used alone;
Can't be used alone_ PTR < int > P (New int (10)) cannot create a break in this way_ PTR object, which is the wrong usage;
2.weak_ptr objects can only point to one shared object_ Object created by PTR, but weak_ptr does not manage shared_ The space life cycle of the object memory pointed to by PTR pointer; This weak_ptr will not increase shared_ Reference count of PTR.
In other words, shared_ The object pointed to by PTR releases space when it releases space, and weak_ptr doesn't matter, although weak_ptr still points to the memory space of the object, as long as shared_ If the reference count of PTR is 0, the memory space of the object will be released;

We know weak_ptr is used to assist shared_ptr is used, so how is it assisted?
First of all, we have to understand what circular reference is.
Now let's set up a scene class: a human with a car; A car needs a person;

Design a member variable shared in the People class_ PTR < car > type pointer;
Design a member variable shared in the Car class_ PTR < people > type pointer;

#include<iostream>
#include<memory>
using namespace std;

class Car; //Pre declaration makes People know Car class
class People
{
public:
	shared_ptr<Car> _car;
	People()
	{
		cout << "People Constructor execution" << endl;
	}
	~People()
	{
		cout << "People Destructor execution" << endl;
	}
};
class Car
{	
public:
	shared_ptr<People> _people;
	Car()
	{
		cout << "car Constructor execution" << endl;
	}
	~Car()
	{
		cout << "car Destructor execution" << endl;
	}

};
void test()
{
	shared_ptr<People> people(new People()); //Open up heap space for People

	shared_ptr<Car> car(new Car()); //Open up the heap space of Car
	//Then let the member variables in the class point to each other's shared_ptr pointer
	people->_car = car;  //This causes the shared to point to the car object_ PTR has 2 reference counts
	car->_people = people;//This will cause the shared to point to the People object_ PTR has 2 reference counts
}
int main()
{
	test();
	return 0;
}

Once I call the test function above, guess what will be output? Are there two normal constructor calls and two destructor calls?

Obviously, when I execute this line of code, I don't display the correct destructor twice, that is, there is a serious problem in this code, that is, memory leakage.

This is also a problem caused by circular reference, which leads to memory leakage. So what is circular reference?
That is, shared_ptr resource memory management, pointing to each other, your shared_ptr points to my sahred_ptr, my shared_ptr points to your shared_ptr´╝Ť
Draw a diagram to better understand the above code

We can clearly see that there is a circular circle of mutual references. When the shared pointer declaration cycle of our people and car ends, that is, when the stack space is destroyed, the reference count of the shared pointer will be reduced by 1, but we find that it is only reduced by 1, not 0, There are still shared pointers of member variables pointing to each other in the heap space, which leads to the problem that the space of the object is not released; As a result, the destructor cannot be executed;

So how can we solve this problem?
In fact, it's easy to solve, just through weak_ptr can be used to solve the problem. Just put shared in any class_ Replace ptr with weak -- ptr can solve the problem of circular reference band, and no other code needs to be changed.

For example, I modified shared in the People class_ ptr<Car> _ Car is weak_ ptr<Car> _ Car, of course, you can also modify it in the car class. Just modify one of them;

#include<iostream>
#include<memory>
using namespace std;

class Car; //Pre declaration makes People know Car class
class People
{
public:
	weak_ptr<Car> _car; //Changed to weak_ptr
	People()
	{
		cout << "People Constructor execution" << endl;
	}
	~People()
	{
		cout << "People Destructor execution" << endl;
	}
};
class Car
{	
public:
	shared_ptr<People> _people;
	Car()
	{
		cout << "car Constructor execution" << endl;
	}
	~Car()
	{
		cout << "car Destructor execution" << endl;
	}

};
void test()
{
	shared_ptr<People> people(new People()); //Open up heap space for People

	shared_ptr<Car> car(new Car()); //Open up the heap space of Car
	//Then let the member variables in the class point to each other's shared_ptr pointer
	people->_car = car;  //Here, the car object reference count of new will not be changed to 2, but will still be 1, because this is a weak_ptr pointer
	car->_people = people;//This will cause the shared to point to the People object_ PTR has 2 reference counts
}
int main()
{
	test();
	return 0;
}

Check the result: it meets our expectations and successfully releases memory

So what is the principle? The principle is very simple. Just draw a picture

Once the car shared pointer of the stack space leaves the scope, the new Car object will be released. Because the shared pointer car of the new Car has only one reference count, the new Car object will call its own destructor. Once it calls its own destructor, it will cause the member variables in the new Car object_ The reference count of people's shared pointer is less than 1. Since the shared pointer of people also leaves the scope in the stack space, that is to say, the reference count of new People's shared pointer will also be less than 1. In this way, the original two reference counts will become 0, and the space of new People will be released

8 unique_ Basic use of PTR

unique_ptr pointer is an exclusive pointer, that is, when executing an object memory, only one unique pointer can point to it, not more than one;

So say: basically no difference from shared_ptr usage, we just need to analyze some common errors;

The structure cannot be copied;
Assignment initialization is not allowed;
No assignment copy is allowed;

unique_ptr<string> p1(new string("hello world!"));

//The following three assignment methods are not applicable, because this is an exclusive pointer, and only one pointer can point to the memory unit of the object string;
unique_ptr<string> p2(p1);
unique_ptr<string> p3 = p1;
unque_ptr<string> p4;
p4 = p1;

C++ 14 also provides a way to use make_unique function template for initialization_ PTR pointer, but C++11 does not support this writing method

unique_ptr<int> p = std::make_unique<int>(100);

9 intelligent pointer selection

If you want to use multiple pointers to point to the same object in the program, choose shared_ptr;
If you want to use a single pointer to point to the same object in the program, choose unique_ptr;

Keywords: C++ Back-end

Added by dineshsjce on Tue, 08 Feb 2022 11:19:51 +0200