C++ 11 smart pointer

Posted by andysez on Fri, 04 Feb 2022 14:25:46 +0100

Function of smart pointer

Using heap memory in C + + programming is a very frequent operation. The application and release of heap memory are managed by programmers themselves. Programmers can improve the efficiency of the program by managing the heap memory themselves, but on the whole, the management of heap memory is troublesome. The concept of intelligent pointer is introduced into C++11 to facilitate the management of heap memory. Using ordinary pointers is easy to cause heap memory leakage (forgetting to release), secondary release, memory leakage in case of program exceptions, etc. using intelligent pointers can better manage heap memory.

Understanding smart pointers requires the following three levels:

From a shallow perspective, smart pointer encapsulates the ordinary pointer by using a technology called RAII (resource acquisition, i.e. initialization), which makes the smart pointer essentially an object and behave like a pointer.
The function of smart pointer is to prevent forgetting to call delete to release memory and forgetting to release memory when program exceptions enter the catch block. In addition, the release timing of the pointer is also very exquisite. Releasing the same pointer many times will cause program crash, which can be solved by smart pointer.
Another function of smart pointer is to convert value semantics into reference semantics. The biggest difference between C + + and Java lies in the semantics. In Java, the following code:
  Animal a = new Animal();

Animal b = a;

 Of course you know, there is only one object generated here, a and b It's just holding the reference of the object. But in C++This is not the case in China,

 Animal a;

 Animal b = a;

 Here, two objects are generated.

The smart pointer is provided after the C++11 version and is contained in the header file_ ptr,unique_ptr,weak_ptr

Use of smart pointer:

shared_ Use of PTR
shared_ptr multiple pointers point to the same object. shared_ptr uses reference count, each shared_ All copies of PTR point to the same memory. Each time it is used, the internal reference count is increased by 1. Each time it is destructed, the internal reference count is reduced by 1. When it is reduced to 0, the heap memory pointed to is automatically deleted. shared_ The reference count inside PTR is thread safe, but the reading of objects needs to be locked.

  • initialization. Smart pointer is a template class. You can specify the type. The incoming pointer is initialized by the constructor. You can also use make_ The shared function is initialized. You cannot assign a pointer to a smart pointer directly. One is a class and the other is a pointer. For example, std::shared_ptr
    p4 = new int(1); Is wrong
  • Copy and assign. Copy increases the reference count of the object by 1, and assignment decreases the reference count of the original object by 1. When the count is 0, the memory is automatically released. The reference count of the object pointed to later is increased by 1 to point to the later object.
  • get function gets the original pointer
  • Be careful not to initialize multiple shared with one original pointer_ PTR, otherwise the same memory will be released twice.
  • Be careful to avoid circular references_ One of the biggest pitfalls of PTR is circular reference. Circular reference will lead to incorrect heap memory release and memory leakage.
#include <iostream>
#include <memory>
using namespace std;

int main()
{
    // Use smart pointer to manage an int heap memory, and the internal reference count is 1
    shared_ptr<int> ptr1(new int(520));
    cout << "ptr1 Managed memory reference count: " << ptr1.use_count() << endl;
    //Call copy constructor
    shared_ptr<int> ptr2(ptr1);
    cout << "ptr2 Managed memory reference count: " << ptr2.use_count() << endl;
    shared_ptr<int> ptr3 = ptr1;
    cout << "ptr3 Managed memory reference count: " << ptr3.use_count() << endl;
    //Call the move constructor
    shared_ptr<int> ptr4(std::move(ptr1));
    cout << "ptr4 Managed memory reference count: " << ptr4.use_count() << endl;
    std::shared_ptr<int> ptr5 = std::move(ptr2);
    cout << "ptr5 Managed memory reference count: " << ptr5.use_count() << endl;

    return 0;
}
ptr1 Managed memory reference count: 1
ptr2 Managed memory reference count: 2
ptr3 Managed memory reference count: 3
ptr4 Managed memory reference count: 3
ptr5 Managed memory reference count: 3

If the shared smart pointer object is initialized by copying, the two objects will manage the same heap memory at the same time, and the reference count corresponding to the heap memory will also increase; If you use the move method to initialize the smart pointer object, you only transfer the ownership of the memory, and the objects managing the memory will not increase, so the reference count of the memory will not change.

#include <iostream>
#include <memory>

int main() {
    {
        int a = 10;
        std::shared_ptr<int> ptra = std::make_shared<int>(a);
        std::shared_ptr<int> ptra2(ptra); //copy
        std::cout << ptra.use_count() << std::endl;

        int b = 20;
        int *pb = &a;
        //std::shared_ptr<int> ptrb = pb;  //error
        std::shared_ptr<int> ptrb = std::make_shared<int>(b);
        ptra2 = ptrb; //assign
        pb = ptrb.get(); //Get original pointer

        std::cout << ptra.use_count() << std::endl;
        std::cout << ptrb.use_count() << std::endl;
    }
}

Through std::make_shared initialization

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

class Test
{
public:
    Test() 
    {
        cout << "construct Test..." << endl;
    }
    Test(int x) 
    {
        cout << "construct Test, x = " << x << endl;
    }
    Test(string str) 
    {
        cout << "construct Test, str = " << str << endl;
    }
    ~Test()
    {
        cout << "destruct Test ..." << endl;
    }
};

int main()
{
    // Use smart pointer to manage an int heap memory, and the internal reference count is 1
    shared_ptr<int> ptr1 = make_shared<int>(520);
    cout << "ptr1 Managed memory reference count: " << ptr1.use_count() << endl;

    shared_ptr<Test> ptr2 = make_shared<Test>();
    cout << "ptr2 Managed memory reference count: " << ptr2.use_count() << endl;

    shared_ptr<Test> ptr3 = make_shared<Test>(520);
    cout << "ptr3 Managed memory reference count: " << ptr3.use_count() << endl;

    shared_ptr<Test> ptr4 = make_shared<Test>("I'm the man who wants to be the pirate king!!!");
    cout << "ptr4 Managed memory reference count: " << ptr4.use_count() << endl;
    return 0;
}

Initialization by reset method

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

int main()
{
    // Use smart pointer to manage an int heap memory, and the internal reference count is 1
    shared_ptr<int> ptr1 = make_shared<int>(520);
    shared_ptr<int> ptr2 = ptr1;
    shared_ptr<int> ptr3 = ptr1;
    shared_ptr<int> ptr4 = ptr1;
    cout << "ptr1 Managed memory reference count: " << ptr1.use_count() << endl;
    cout << "ptr2 Managed memory reference count: " << ptr2.use_count() << endl;
    cout << "ptr3 Memory reference count: " << ptr3.use_count() << endl;
    cout << "ptr4 Managed memory reference count: " << ptr4.use_count() << endl;

    ptr4.reset();
    cout << "ptr1 Managed memory reference count: " << ptr1.use_count() << endl;
    cout << "ptr2 Managed memory reference count: " << ptr2.use_count() << endl;
    cout << "ptr3 Managed memory reference count: " << ptr3.use_count() << endl;
    cout << "ptr4 Managed memory reference count: " << ptr4.use_count() << endl;

    shared_ptr<int> ptr5;
    ptr5.reset(new int(250));
    cout << "ptr5 Managed memory reference count: " << ptr5.use_count() << endl;

    return 0;
}
ptr1 Managed memory reference count: 4
ptr2 Managed memory reference count: 4
ptr3 Managed memory reference count: 4
ptr4 Managed memory reference count: 4
    
ptr1 Managed memory reference count: 3
ptr2 Managed memory reference count: 3
ptr3 Managed memory reference count: 3
ptr4 Managed memory reference count: 0
    
ptr5 Managed memory reference count: 1

An uninitialized shared smart pointer can be initialized through the reset method. When there is a value in the smart pointer, calling reset will reduce the reference count by 1.

Get original pointer

Corresponding to the basic data type, the memory effect managed by operating the smart pointer and operating the smart pointer is the same, and the data can be read and written directly. However, if the shared smart pointer manages an object, you need to take out the address of the original memory before operation. You can call the get() method provided by the shared smart pointer class to get the original address. Its function prototype is as follows:

T* get() const noexcept;

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

int main()
{
    int len = 128;
    shared_ptr<char> ptr(new char[len]);
    // Get the original address of the pointer
    char* add = ptr.get();
    memset(add, 0, len);
    strcpy(add, "I'm the man who wants to be the pirate king!!!");
    cout << "string: " << add << endl;
    
    shared_ptr<int> p(new int);
    *p = 100;
    cout << *p.get() << "  " << *p << endl;
    
    return 0;
}

Specify the delegator

When the reference count corresponding to the memory managed by the smart pointer becomes 0, this memory will be destructed by the smart pointer. In addition, when initializing the smart pointer, we can also specify the deletion action ourselves. The function corresponding to the deletion operation is called the delegator. The essence of the delegator function is a callback function. We only need to implement it, and its call is completed by the smart pointer.

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

// User defined delete function to free int memory
void deleteIntPtr(int* p)
{
    delete p;
    cout << "int Type memory was freed...";
}

int main()
{
    shared_ptr<int> ptr(new int(250), deleteIntPtr);
    return 0;
}

Using shared in C++11_ When PTR manages dynamic arrays, you need to specify a delegator because STD:: shared_ The default deletion device of PTR does not support array objects. The specific processing code is as follows:

int main()
{
    shared_ptr<int> ptr(new int[10], [](int* p) {delete[]p; });
    return 0;
}

In addition, we can also encapsulate a make by ourselves_ shared_ Array method to make shared_ptr supports arrays. The code is as follows:

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

template <typename T>
shared_ptr<T> make_share_array(size_t size)
{
    // Return anonymous object
    return shared_ptr<T>(new T[size], default_delete<T[]>());
}

int main()
{
    shared_ptr<int> ptr1 = make_share_array<int>(10);
    cout << ptr1.use_count() << endl;
    shared_ptr<char> ptr2 = make_share_array<char>(128);
    cout << ptr2.use_count() << endl;
    return 0;
}

unique_ Use of PTR

unique_ptr "unique" has the object it refers to, and there can only be one unique at a time_ PTR points to a given object (by prohibiting copy semantics and only moving semantics). Unique compared with the original pointer_ PTR is used for its RAII characteristics, so that dynamic resources can be released in case of exceptions. unique_ Life cycle of PTR pointer itself: from unique_ The PTR pointer starts when it is created until it leaves the scope. When leaving the scope, if it points to an object, it will destroy the object it refers to (the delete operator is used by default, and the user can specify other operations). unique_ Relationship between PTR pointer and the object it refers to: during the life cycle of smart pointer, the object it refers to can be changed. For example, when creating smart pointer, it can be specified through constructor, re specified through reset method, release ownership through release method, and transfer ownership through mobile semantics.

#include <iostream>
#include <memory>

int main() {
    {
        std::unique_ptr<int> uptr(new int(10));  //Binding dynamic objects
        //std::unique_ ptr<int> uptr2 = uptr;  // Cannot assign value
        //std::unique_ ptr<int> uptr2(uptr);  // Cannot copy
        std::unique_ptr<int> uptr2 = std::move(uptr); //Conversion of ownership
        uptr2.release(); //Release ownership
    }
    //Memory freed when the scope of uptr is exceeded
}

weak_ Use of PTR

weak_ptr is to cooperate with shared_ PTR is a kind of intelligent pointer introduced by PTR, because it does not have the behavior of ordinary pointer and does not overload operator * and - >. Its greatest function is to assist shared_ PTR works to observe the use of resources like a bystander. weak_ptr can be from a shared_ PTR or another weak_ptr object construction to obtain the observation right of resources. But weak_ptr has no shared resources, and its construction will not increase the pointer reference count. Use weak_ptr member function use_count() can observe the reference count of resources, and the function of another member function expired() is equivalent to use_count()==0, but faster, indicating that the observed resources (that is, the resources managed by shared_ptr) no longer exist. weak_ptr can use a very important member function, lock(), to extract from the observed shared_ PTR gets an available shared_ PTR object to manipulate resources. But when expired()==true, the lock() function will return a shared pointer that stores null pointers_ ptr

#include <iostream>
#include <memory>

int main() {
    {
        std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
        std::cout << sh_ptr.use_count() << std::endl;

        std::weak_ptr<int> wp(sh_ptr);
        std::cout << wp.use_count() << std::endl;

        if(!wp.expired()){
            std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
            *sh_ptr = 100;
            std::cout << wp.use_count() << std::endl;
        }
    }
    //delete memory
}
#include <iostream>
#include <memory>
using namespace std;

int main() 
{
    shared_ptr<int> sp(new int);

    weak_ptr<int> wp1;
    weak_ptr<int> wp2(wp1);
    weak_ptr<int> wp3(sp);
    weak_ptr<int> wp4;
    wp4 = sp;
    weak_ptr<int> wp5;
    wp5 = wp3;

    cout << "use_count: " << endl;
    cout << "wp1: " << wp1.use_count() << endl;
    cout << "wp2: " << wp2.use_count() << endl;
    cout << "wp3: " << wp3.use_count() << endl;
    cout << "wp4: " << wp4.use_count() << endl;
    cout << "wp5: " << wp5.use_count() << endl;
    return 0;
}
use_count:
wp1: 0
wp2: 0
wp3: 1
wp4: 1
wp5: 1
 Through the printed results, we can know that although the weak reference smart pointer wp3,wp4,wp5 The monitored resource is the same, but its reference count has not changed, which is further proved weak_ptr Just monitoring resources, not managing resources.
#include <iostream>
#include <memory>
using namespace std;

int main()
{
    shared_ptr<int> sp1, sp2;
    weak_ptr<int> wp;

    sp1 = std::make_shared<int>(520);
    wp = sp1;
    sp2 = wp.lock();
    cout << "use_count: " << wp.use_count() << endl;

    sp1.reset();
    cout << "use_count: " << wp.use_count() << endl;

    sp1 = wp.lock();
    cout << "use_count: " << wp.use_count() << endl;

    cout << "*sp1: " << *sp1 << endl;
    cout << "*sp2: " << *sp2 << endl;

    return 0;
}

use_count: 2
use_count: 1
use_count: 2
*sp1: 520
*sp2: 520
  • sp2 = wp.lock(); By calling the lock () method, you get a method for managing weak_ptr
    The shared smart pointer object of the resource monitored by the object. Use this object to initialize sp2. At this time, the reference count of the monitored resource is 2
  • sp1.reset(); Shared smart pointer Sp1 is reset, weak_ The reference count of the resources monitored by the PTR object is decremented by 1
  • sp1 = wp.lock();sp1 is reinitialized and managed as weak_ The resources monitored by the PTR object, so the reference count is incremented by 1
  • The shared smart pointer objects sp1 and sp2 manage the same piece of memory, so the result in the final printed memory is the same, both 520

Solve circular reference problem

Consider a simple object modeling - parents and children: a parent has a child, a child knowledge / her parent. It's easy to write in Java. You don't have to worry about memory leakage or dangling pointers. As long as myChild and myParent are initialized correctly, Java programmers don't have to worry about access errors. Whether a handle is valid only needs to judge whether it is non null.

public class Parent
{
  private Child myChild;
}
public class Child
{
  private Parent myParent;
}

In C + +, you have to think about resource management. If the original pointer is used as a member, who releases Child and Parent? So how to ensure the validity of the pointer? How to prevent dangling pointers? These problems are troublesome in C + + object-oriented programming. Now we can use smart pointer to transform object semantics into value semantics_ PTR can easily solve the problem of life cycle without worrying about dangling pointers. However, there is a problem of circular reference in this model. Note that one of the pointers should be weak_ptr.
The original pointer is error prone:

#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
private:
    Child* myChild;
public:
    void setChild(Child* ch) {
        this->myChild = ch;
    }

    void doSomething() {
        if (this->myChild) {

        }
    }

    ~Parent() {
        delete myChild;
    }
};

class Child {
private:
    Parent* myParent;
public:
    void setPartent(Parent* p) {
        this->myParent = p;
    }
    void doSomething() {
        if (this->myParent) {

        }
    }
    ~Child() {
        delete myParent;
    }
};

int main() {
    {
        Parent* p = new Parent;
        Child* c =  new Child;
        p->setChild(c);
        c->setPartent(p);
        delete c;  //only delete one
    }
    return 0;
}

Memory leakage of smart pointer circular reference:

#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
private:
    std::shared_ptr<Child> ChildPtr;
public:
    void setChild(std::shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        if (this->ChildPtr.use_count()) {

        }
    }

    ~Parent() {
    }
};

class Child {
private:
    std::shared_ptr<Parent> ParentPtr;
public:
    void setPartent(std::shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    std::weak_ptr<Parent> wpp;
    std::weak_ptr<Child> wpc;
    {
        std::shared_ptr<Parent> p(new Parent);
        std::shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        std::cout << p.use_count() << std::endl; // 2
        std::cout << c.use_count() << std::endl; // 2
    }
    std::cout << wpp.use_count() << std::endl;  // 1
    std::cout << wpc.use_count() << std::endl;  // 1
    return 0;
}

The correct approach of smart pointer:

#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
private:
    //std::shared_ptr<Child> ChildPtr;
    std::weak_ptr<Child> ChildPtr;
public:
    void setChild(std::shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        //new shared_ptr
        if (this->ChildPtr.lock()) {

        }
    }

    ~Parent() {
    }
};

class Child {
private:
    std::shared_ptr<Parent> ParentPtr;
public:
    void setPartent(std::shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    std::weak_ptr<Parent> wpp;
    std::weak_ptr<Child> wpc;
    {
        std::shared_ptr<Parent> p(new Parent);
        std::shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        std::cout << p.use_count() << std::endl; // 2
        std::cout << c.use_count() << std::endl; // 1
    }
    std::cout << wpp.use_count() << std::endl;  // 0
    std::cout << wpc.use_count() << std::endl;  // 0
    return 0;
}

Design and implementation of intelligent pointer

The following is a demo of a simple smart pointer. The smart pointer class associates a counter with the object pointed to by the class, and the reference count tracks how many objects of the class share the same pointer. Each time a new object of the class is created, the pointer is initialized and the reference count is set 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 in the left operand (if the reference count is reduced to 0, the object will be deleted) and increases the reference count of the object referred to in the right operand; When the destructor is called, the constructor decreases the reference count (if the reference count decreases to 0, the underlying object is deleted). Smart pointer is a class that simulates pointer action. All smart pointers overload the - > and * operators. Smart pointer has many other functions. The more useful one is automatic destruction. This is mainly to use the limited scope of stack object and the destructor of temporary object (limited scope Implementation) to release memory.

#include <iostream>
#include <memory>

template<typename T>
class SmartPointer {
private:
    T* _ptr;
    size_t* _count;
public:
    SmartPointer(T* ptr = nullptr) :
            _ptr(ptr) {
        if (_ptr) {
            _count = new size_t(1);
        } else {
            _count = new size_t(0);
        }
    }

    SmartPointer(const SmartPointer& ptr) {
        if (this != &ptr) {
            this->_ptr = ptr._ptr;
            this->_count = ptr._count;
            (*this->_count)++;
        }
    }

    SmartPointer& operator=(const SmartPointer& ptr) {
        if (this->_ptr == ptr._ptr) {
            return *this;
        }

        if (this->_ptr) {
            (*this->_count)--;
            if (this->_count == 0) {
                delete this->_ptr;
                delete this->_count;
            }
        }

        this->_ptr = ptr._ptr;
        this->_count = ptr._count;
        (*this->_count)++;
        return *this;
    }

    T& operator*() {
        assert(this->_ptr == nullptr);
        return *(this->_ptr);

    }

    T* operator->() {
        assert(this->_ptr == nullptr);
        return this->_ptr;
    }

    ~SmartPointer() {
        (*this->_count)--;
        if (*this->_count == 0) {
            delete this->_ptr;
            delete this->_count;
        }
    }

    size_t use_count(){
        return *this->_count;
    }
};

int main() {
    {
        SmartPointer<int> sp(new int(10));
        SmartPointer<int> sp2(sp);
        SmartPointer<int> sp3(new int(20));
        sp2 = sp3;
        std::cout << sp.use_count() << std::endl;
        std::cout << sp3.use_count() << std::endl;
    }
    //delete operator
}

Topics: C++ Back-end