How to design and use smart Pointers in C++

  • 2020-04-01 21:30:33
  • OfStack

        Smart pointer is a class that stores Pointers to dynamically allocated (heap) objects. It is used for lifetime control to ensure that dynamically allocated objects are automatically and correctly destroyed to prevent memory leaks. One of its common implementation techniques is the use of reference count. The smart pointer class associates a counter with the object the class points to, and the reference count keeps track of how many objects in the class share the same pointer. Each time a new object of the class is created, initialize the pointer and set the reference count to 1. When an object is created as a copy of another object, the copy constructor copies the pointer and increases the corresponding reference count. When assigning an object, the assignment operator reduces the reference count of the object referred to by the left operand (if the reference count is reduced to 0, the object is deleted) and increases the reference count of the object referred to by the right operand. When the destructor is called, the constructor reduces the reference count (if the reference count is reduced to 0, the underlying object is removed).
      A smart pointer is a class that simulates pointer action. All smart Pointers are overloaded. And the * operator. Smart Pointers have many other features, the more useful of which is auto-destruction. This is done primarily by using the finite scope of the stack object and the temporary object (finite scope implementation) destructor to free memory. Of course, smart Pointers don't stop there. They include the ability to modify source objects while copying. Smart Pointers are designed differently depending on the requirements (copy on write, assignment that is, releasing an object to have permissions, reference counting, etc., transfer of control, etc.). Auto_ptr is a common smart pointer.
        Smart Pointers are usually implemented using class templates:
 
template <class T> 
class smartpointer 
{ 
private: 
T *_ptr; 
public: 
smartpointer(T *p) : _ptr(p) //The constructor
{ 
} 
T& operator *() //Overloading the * operator
{ 
return *_ptr; 
} 
T* operator ->() //Overloading -> The operator
{ 
return _ptr; 
} 
~smartpointer() //The destructor
{ 
delete _ptr; 
} 
}; 

There are two classic strategies for reference counting, one of which will be used here. In the method used here, a separate concrete class needs to be defined to encapsulate reference counting and related Pointers:
 
//Defines a U_Ptr class that is used only by the HasPtr class to encapsulate the use of counts and associated Pointers
//All members of this class are private, and we don't want ordinary users to use the U_Ptr class, so it doesn't have any public members
//Set the HasPtr class as a friend so that its members can access the members of U_Ptr
class U_Ptr 
{ 
friend class HasPtr; 
int *ip; 
size_t use; 
U_Ptr(int *p) : ip(p) , use(1) 
{ 
cout << "U_ptr constructor called !" << endl; 
} 
~U_Ptr() 
{ 
delete ip; 
cout << "U_ptr distructor called !" << endl; 
} 
}; 

            The asptr class needs a destructor to delete Pointers. However, destructors cannot unconditionally delete Pointers.
          The condition is the reference count. If the object is referred to by two Pointers, deleting one of the Pointers does not call the destructor of the pointer because there is another pointer to the object. It seems that the smart pointer is mainly to prevent improper destructive behavior, to prevent the emergence of dangling pointer.
< img Alt = "" border = 0 SRC =" / / files.jb51.net/file_images/article/201211/201211281133036.jpg ">
        As shown in the figure above, HasPtr is the smart pointer, and U_Ptr is the counter; There is a use and pointer IP, and use records how many HasPtr objects the * IP object is referred to. Suppose now that two more HasPtr objects p1 and p2 point to U_Ptr, then I delete ; P1, use variable will subtract 1 from itself, & sponge; U_Ptr will not be destructed, so the object pointed to by U_Ptr will not be destructed, so p2 will still point to the original object instead of becoming a dangling pointer. When delete p2, the use variable subtracts 1 from itself to 0. At this point, the U_Ptr object is destructed, so the object pointed to by U_Ptr is also destructed to ensure that there is no memory leak.  
      Classes that contain Pointers need to pay special attention to replication control because when you copy a pointer, you only copy the address in the pointer, not the object that the pointer points to.
      Most C++ classes manage pointer members in one of three ways
      (1) regardless of pointer members. When copying, only the pointer is copied, not the object to which the pointer points. When one pointer frees the space of the object to which it points, the others become dangling Pointers. This is an extreme
      (2) when copying, that is, copying the pointer, also copies the object that the pointer points to. This may result in a waste of space. Because a copy of the object that the pointer points to is not necessarily necessary.
    (3) the third way is a compromise. Use a helper class to manage the replication of Pointers. The original class has a pointer to the secondary class, and the data members of the secondary class are a counter and a pointer to the original (this is the smart pointer implementation).
        In fact, the reference count of a smart pointer is similar to Java's garbage collection mechanism: Java's garbage is simply determined to be garbage if an object has no reference to it. The system can be recycled.
        The declaration of the HasPtr smart pointer is as follows: save a pointer to the U_Ptr object, and the U_Ptr object points to the actual int base object. The code is as follows:
 
#include<iostream> 
using namespace std; 

//Defines a U_Ptr class that is used only by the HasPtr class to encapsulate the use of counts and associated Pointers
//All members of this class are private, and we don't want ordinary users to use the U_Ptr class, so it doesn't have any public members
//Set the HasPtr class as a friend so that its members can access the members of U_Ptr
class U_Ptr 
{ 
    friend class HasPtr; 
    int *ip; 
    size_t use; 
    U_Ptr(int *p) : ip(p) , use(1) 
    { 
        cout << "U_ptr constructor called !" << endl; 
    } 
    ~U_Ptr() 
    { 
        delete ip; 
        cout << "U_ptr distructor called !" << endl; 
    } 
}; 

class HasPtr 
{ 
public: 
    //Constructor: p is a pointer to an int object that has been dynamically created
    HasPtr(int *p, int i) : ptr(new U_Ptr(p)) , val(i) 
    { 
        cout << "HasPtr constructor called ! " << "use = " << ptr->use << endl; 
    } 

    //Copy constructor: copies the member and increments the count by 1
    HasPtr(const HasPtr& orig) : ptr(orig.ptr) , val(orig.val) 
    { 
        ++ptr->use; 
        cout << "HasPtr copy constructor called ! " << "use = " << ptr->use << endl; 
    } 

    //Assignment operator
    HasPtr& operator=(const HasPtr&); 

    //Destructor: if the count is 0, the U_Ptr object is deleted
    ~HasPtr() 
    { 
        cout << "HasPtr distructor called ! " << "use = " << ptr->use << endl; 
        if (--ptr->use == 0) 
            delete ptr; 
    } 

    //Fetch data member
    int *get_ptr() const 
    { 
        return ptr->ip; 
    } 
    int get_int() const 
    { 
        return val; 
    } 

    //Modify data member
    void set_ptr(int *p) const 
    { 
        ptr->ip = p; 
    } 
    void set_int(int i) 
    { 
        val = i; 
    } 

    //Returns or modifies the underlying int object
    int get_ptr_val() const 
    { 
        return *ptr->ip; 
    } 
    void set_ptr_val(int i) 
    { 
        *ptr->ip = i; 
    } 
private: 
    U_Ptr *ptr;   //Points to the use counting class U_Ptr
    int val; 
}; 
HasPtr& HasPtr::operator = (const HasPtr &rhs)  //Note that the assignment operator here adds 1 to the RHS usage technique before reducing the use count of the operation number to prevent self-assignment
{ 
    //Increases the use count in the right operand
    ++rhs.ptr->use; 
    //Subtract 1 from the usage count of the left operand object. If the usage count of the object is reduced to 0, the object is deleted
    if (--ptr->use == 0) 
        delete ptr; 
    ptr = rhs.ptr;   //Copy the U_Ptr pointer
    val = rhs.val;   //Copy int member
    return *this; 
} 

int main(void) 
{ 
    int *pi = new int(42); 
    HasPtr *hpa = new HasPtr(pi, 100);    //The constructor
    HasPtr *hpb = new HasPtr(*hpa);     //Copy constructor
    HasPtr *hpc = new HasPtr(*hpb);     //Copy constructor
    HasPtr hpd = *hpa;     //Copy constructor

    cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl; 
    hpc->set_ptr_val(10000); 
    cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl; 
    hpd.set_ptr_val(10); 
    cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl; 
    delete hpa; 
    delete hpb; 
    delete hpc; 
    cout << hpd.get_ptr_val() << endl; 
    return 0; 
} 

The assignment operator is a bit cumbersome here, but let me chart it out:
Suppose now there are two smart Pointers, p1 and p2, one pointing to the memory with the content of 42 and the other to the memory with the content of 100, as shown in the following figure:
< img Alt = "" border = 0 SRC =" / / files.jb51.net/file_images/article/201211/201211281133037.jpg ">
Now, I'm going to do the assignment. P2 is equal to p1. Compare that to the top

 
HasPtr& operator=(const HasPtr&); //Assignment operator
At this point, RHS is p1. First, add p1 to the use of PTR,

 
++rhs.ptr->use; //Increases the use count in the right operand
Then, do:

 
if (--ptr->use == 0) 
delete ptr; 
Since the object that p2 originally pointed to is no longer pointed to by p2, the object will lose a pointer to point to.
Now, this is true. Because use of u2 is 1. So, run the destructor of U_Ptr, and in the destructor of U_Ptr, do the delete IP operation, so free up memory, there is no memory leak problem.
The following operation is quite natural and needless to say:

 
ptr = rhs.ptr; //Copy the U_Ptr pointer
val = rhs.val; //Copy int member
return *this; 
Once the assignment is done, it should look like the following figure. The changes are shown in red:
< img Alt = "" border = 0 SRC =" / / files.jb51.net/file_images/article/201211/201211281133038.jpg ">
Also note that when overloading the assignment operator, it is important to check for self-assignment.
As shown in the picture:
< img Alt = "" border = 0 SRC =" / / files.jb51.net/file_images/article/201211/201211281133039.jpg ">
Now, let's do p1 = p1. So, first of all, u1. Use self-increment 1, is 2; Then, u1.use subtracts 1 from itself to 1. Then the delete operation will not be performed and the rest of the operation will proceed smoothly. According to the C++ primer, "this assignment operator adds 1 to the RHS usage count before reducing the left operand usage count to prevent its own assignment." Well, that's how I understand it. Of course, one in the assignment operator function can do as usual:

 
if(this == &rhs) 
return *this; 
The running results are as follows:
< img Alt = "" border = 0 SRC =" / / files.jb51.net/file_images/article/201211/2012112811330310.png ">

Related articles: