C + + 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. However, using ordinary pointers is easy to cause problems such as memory leakage (forgetting to release), secondary release, memory leakage in case of program exceptions, etc. All C++11 introduces smart pointers.

Raw pointers are prone to memory leaks

In C language, malloc() function is used to allocate memory, free() function releases memory, and the corresponding keywords in C + + are new and delete. Malloc () only allocates memory, while new goes further, not only allocates memory, but also calls the constructor for initialization;

int main()
{
    // malloc returns void*
    int* argC = (int*)malloc(sizeof(int));
    free(argC);

    char *age = new int(25); // Did two things: 1. Allocate memory 2. Initialize
    delete age;
}

new and delete must appear in pairs. Sometimes they accidentally forget to delete. Sometimes it is difficult to judge whether they should delete in this place. This is related to the life cycle of the resource. Whether the resource is managed by me or another class (other classes may use it). If it is managed by others, it will be deleted by others.

If you need to manage your own memory, you'd better transfer your own resources. In this way, you can know that the resource should be managed by yourself.

char *getName(char* v, size_t bufferSize) {
    //do something
    return v;
}

The above is still a small problem. Be careful and take a closer look at the documents. There is still a chance to avoid these situations. However, after the introduction of the concept of exception in C + +, the control flow of the program has changed fundamentally. Memory leakage may still occur when delete is written. Examples are as follows:

void badThing(){
    throw 1;// Throw an exception 
}

void test() {
    char* a = new char[1000];

    badThing();
    // do something
    delete[] a;
}
int main() {
    try {
        test();
    }
    catch (int i){
        cout << "error happened " << i << endl;
    }
}

The above new and delete appear in pairs, but the program throws an exception in the middle. Because it is not captured immediately, the program exits from here and does not execute to delete. Memory leakage still occurs.

Using constructors and destructors is very powerful. You can use constructors and destructors to solve the above memory leakage problems, such as:

class SafeIntPointer {
public:
    explicit SafeIntPointer(int v) : m_value(new int(v)) { }
    ~SafeIntPointer() {
        delete m_value;
        cout << "~SafeIntPointer" << endl;
    }
    int get() { return *m_value; }
private:
    int* m_value;
};
void badThing(){
    throw 1;// Throw an exception 
}

void test() {
    SafeIntPointer a(5);

    badThing();
}

int main() {
    try {
        test();
    }
    catch (int i){
        cout << "error happened " << i << endl;
    }
}

// result
// ~SafeIntPointer
// error happened 1

You can see that even if an exception occurs, it can ensure the successful execution of the destructor! The example here is that only one person uses this resource. I release it without using it. However, there is another case where a resource is used by many people in common. It can only be released when everyone no longer uses it. For this problem, it is necessary to add a reference count to the SafeIntPoint above, as follows:

class SafeIntPointer {
public:
    explicit SafeIntPointer(int v) : m_value(new int(v)), m_used(new int(1)) { }
    ~SafeIntPointer() {
        cout << "~SafeIntPointer" << endl;
        (*m_used) --; // Reference count minus 1
        if(*m_used <= 0){
            delete m_used;
            delete m_value;
            cout << "real delete resources" << endl;
        }
    }
    
    SafeIntPointer(const SafeIntPointer& other) {
        m_used = other.m_used;
        m_value = other.m_value;
        (*m_used)++; // Reference count plus 1
    }
    SafeIntPointer& operator= (const SafeIntPointer& other) {
        if (this == &other) // Avoid self assignment!!
           return *this;

        m_used = other.m_used;
        m_value = other.m_value;
        (*m_used)++; // Reference count plus 1
        return *this;
    }

    int get() { return *m_value; }
    int getRefCount() {
        return *m_used;
    }

private:
    int* m_used; // Reference count
    int* m_value;
};

int main() {
    SafeIntPointer a(5);
    cout << "ref count = " << a.getRefCount() << endl;
    SafeIntPointer b = a;
    cout << "ref count = " << a.getRefCount() << endl;
    SafeIntPointer c = b;
    cout << "ref count = " << a.getRefCount() << endl;
}

/*
ref count = 1
ref count = 2
ref count = 3
~SafeIntPointer
~SafeIntPointer
~SafeIntPointer
real delete resources
*/

You can see that for each assignment, the reference count is increased by one. Finally, after each destruct, the reference count is subtracted by one. The resource is not really released until the reference count is 0. It is still difficult to write a wrapper class for correctly managing resources. The above example is not thread safe. It can only belong to a toy and can hardly be used in practical projects.

Smart Pointer is introduced into C++11. It uses a technology called RAII (resource acquisition, i.e. initialization) to encapsulate the ordinary pointer as a stack object. When the life cycle of the stack object ends, the requested memory will be released in the destructor to prevent memory leakage. This allows only a pointer to be an object in essence, but behave like a pointer.

Smart pointers are mainly divided into shared_ptr,unique_ptr and weak_ There are three types of PTR. When using, you need to refer to the shared in the header file. C++11_ PTR and weak_ptr is implemented with reference to the boost library.

shared_ptr shared smart pointer

shared_ Initialization of PTR

The safest way to allocate and use dynamic memory is to call a make_ Standard library functions for sjhared. This function allocates an object in dynamic memory, initializes it, and returns the shared to this object_ ptr. Like smart pointers, make_shared is also defined in the header file memory.

// Points to a shared int with a value of 42_ ptr
shared_ptr<int> p3 = make_shared<int>(42);

// p4 points to a string with a value of "9999999999"
shared_ptr<string> p4 = make_shared<string>(10,'9');

// p5 points to a value initialized int
shared_ptr<int> p5 = make_shared<int>();

We can also initialize the smart pointer with the pointer returned by new, but the smart pointer function that accepts pointer parameters is explicit. Therefore, we cannot implicitly convert a built-in pointer to a smart pointer. We must initialize a smart pointer in the form of direct initialization:

shared_ptr<int> pi = new int (1024); // The error must be in the form of direct initialization
shared_ptr<int> p2 (new int(1024));  // Correct: the direct initialization form is used to initialize a smart pointer:

For the same reason, one returns shared_ptr's function cannot implicitly convert a normal pointer in its return statement:

shared_ptr<int> clone(int p)
{
    return new int(p); // Error: implicitly converted to shared_ ptr<int>
}
shared_ptr Basic use of
std::shared_ptr The basic use of is very simple. You can see it by looking at a few examples:
#include <memory>
#include <iostream>

class Test
{
public:
    Test()
    {
        std::cout << "Test()" << std::endl;
    }
    ~Test()
    {
        std::cout << "~Test()" << std::endl;
    }
};

int main()
{
    std::shared_ptr<Test> p1 = std::make_shared<Test>();
    std::cout << "1 ref:" << p1.use_count() << std::endl;
    {
        std::shared_ptr<Test> p2 = p1;
        std::cout << "2 ref:" << p1.use_count() << std::endl;
    }
    std::cout << "3 ref:" << p1.use_count() << std::endl;
    return 0;
}

//output
//Test()
//1 ref:1
//2 ref:2
//3 ref:1
//~Test()

The code is interpreted as follows:

  • std::make_ new operator is called in shared to allocate memory;
  • Second p1.use_ The reason why count () is displayed as 2 is that the reference object p2 is added, and the scope of p2 ends with the end of curly braces, so the reference count of P1 changes back to 1. With the end of main function, the scope of P1 ends. At this time, it is detected that the count is 1. When P1 is destroyed, call P1's destructor to delete the previously allocated memory space;

Shared is listed below_ PTR unique operation:

make_shared<T>(args) // Returns a shared_ptr, pointing to a dynamically allocated object of type T. Initialize this object with args
shared_ptr<T> p(q) // p is shared_ Copy of PTR q; This action increments the reference count in q. The pointer in q must be convertible to T*
p = q // Both p and q are shared_ptr, the saved pointers must be able to convert to each other. This operation decrements the reference count in p and increments the reference count in q. If the reference count in p becomes 0, the original memory managed by it is released
p.unique() // If p.use_count() is 1 and returns true; Otherwise, false is returned
p.use_count() // Returns the number of smart pointers of objects shared with p; May be slow, mainly for debugging

Here are some changes to shared_ Other methods of PTR:

p.reset () //If p is the only shared that points to its object_ PTR, reset will release this object.
p.reset(q) //If the optional parameter built-in pointer q is passed, P will point to Q, otherwise P will be set to null.
p.reset(q, d) //If parameter d is also passed, d is called instead of delete to release q

weak_ Smart pointer for PTR weak reference

shared_ptr One of the biggest pitfalls of is circular reference. Circular reference will cause heap memory not to be released correctly, resulting in memory leakage. Take the following example:
#include <iostream>
#include <memory>

class Parent;  // Pre declaration of Parent class

class Child {
public:
    Child() { std::cout << "hello child" << std::endl; }
    ~Child() { std::cout << "bye child" << std::endl; }

    std::shared_ptr<Parent> father;
};

class Parent {
public:
    Parent() { std::cout << "hello Parent" << std::endl; }
    ~Parent() { std::cout << "bye parent" << std::endl; }

    std::shared_ptr<Child> son;
};

void testParentAndChild() {

}

int main() {
    std::shared_ptr<Parent> parent(new Parent());  // 1 resource A
    std::shared_ptr<Child> child(new Child());  // 2 resource B
    parent->son = child;     // 3   child.use_count() == 2 and parent.use_count() == 1 
    child->father = parent;  // 4   child.use_count() == 2 and parent.use_count() == 2

    return 0;
}

/*
Output:
hello Parent
hello child
*/

I was surprised to find that shared was used_ PTR manages resources without calling the destructors of Parent and Child, which means that the resources are not released in the end! A memory leak occurred.

analysis:

  • When executing the statement No. 1, A shared smart pointer p is constructed, and the resources it manages are called resource A (object generated by new Parent()). Statement 2 constructs A shared smart pointer c to manage resource B (object generated by new child()). At this time, the reference count of resources A and B is 1, because only one smart pointer manages them. When executing statement 3, It is A smart pointer assignment operation. The reference count of resource B becomes 2. Similarly, after executing statement 4, the reference count of resource A becomes 2.
  • When the function scope is given, because the order of destructor and construction is opposite, the shared smart pointer c will be destructed first, and the reference count of resource B becomes 1; Next, continue to destruct the shared smart pointer p, and the reference count of resource A becomes 1. Since the reference counts of resources A and B are not 1, it indicates that there are shared smart pointers using them, so the destructor of resources will not be called!
  • This situation is an endless cycle. If the reference count of resource A wants to become 0, resource B must be destructed first (so as to destruct the shared smart pointer of internal management resource A). If the reference count of resource B wants to become 0, it has to rely on the destruct of resource A, so it falls into an endless cycle.
weak_ptr How to solve the problem of cross reference
 To solve the above circular reference problem, we can only introduce a new smart pointer std::weak_ptr std::weak_ptr What are the characteristics? And std::shared_ptr The biggest difference is that when assigning values, it will not cause the smart pointer count to increase.

weak_ptr Designed to work with shared_ptr Working together can be from one shared_ptr Or another one weak_ptr Object construction to obtain the observation right of resources. but weak_ptr Without shared resources, its construction will not cause the pointer reference count to increase.

Similarly, in weak_ptr Deconstruction does not reduce the reference count. It is just a quiet observer, weak_ptr No overload operator*and->,This is intentional because it does not share pointers and cannot operate on resources, which is the reason why it is weak.

If you want to manipulate resources, you must use a very important member function lock()From the observed shared_ptr Get an available shared_ptr Object to manipulate resources.
When we create a weak_ptr Use a shared_ptr To initialize it:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp weakly shared p; The reference count for P has not changed

We use it based on the above code std::weak_ptr Modify as follows:
#include <iostream>
#include <memory>

class Parent;  // Pre declaration of Parent class

class Child {
public:
    Child() { std::cout << "hello child" << std::endl; }
    ~Child() { std::cout << "bye child" << std::endl; }

    // Test function
    void testWork()
    {
        std::cout << "testWork()" << std::endl;
    }

    std::weak_ptr<Parent> father;
};

class Parent {
public:
    Parent() { std::cout << "hello Parent" << std::endl; }
    ~Parent() { std::cout << "bye parent" << std::endl; }

    std::weak_ptr<Child> son;
};

void testParentAndChild() {

}

int main() {
    std::shared_ptr<Parent> parent(new Parent());
    std::shared_ptr<Child> child(new Child());
    parent->son = child;
    child->father = parent;
    std::cout << "parent_ref:" << parent.use_count() << std::endl;
    std::cout << "child_ref:" << child.use_count() << std::endl;

    // std::weak_ptr type is converted to std::shared_ptr type to call internal member functions
    std::shared_ptr<Child> tmp = parent.get()->son.lock();
    tmp->testWork();
    std::cout << "tmp_ref:" << tmp.use_count() << std::endl;

    return 0;
}

/*
Output:
hello Parent
hello child
parent_ref:1
child_ref:1
testWork()
tmp_ref:2
bye child
bye parent
*/

From the running results of the above code, we can see:

  • All objects can be released normally in the end, and there will be no problem that the memory in the previous example is not released;
  • Before parent and child exit in the main function, the reference count is 1, that is, for STD:: weak_ The mutual reference of PTR will not increase the count.

weak_ptr common operations

weak_ptr<T> w;	// Empty weak_ptr can point to an object of type T
weak_ptr<T> w(shared_ptr p);	// Weak pointing to the same object as p_ PTR, t must be convertible to the type pointed to by sp
w = p;	// p can be shared_ptr or weak_ptr, w and p share objects after assignment
w.reset();	// weak_ptr is set to null
w.use_count();	// Shared of objects shared with w_ PTR count
w.expired();	// w.use_ If count() is 0, it returns true; otherwise, it returns false
w.lock();	// w.expired() is true and returns null shared_ptr; Otherwise, the shared to W is returned_ ptr
unique_ptr Basic use of
unique_ptr Compared with the other two smart pointers, it is simpler shared_ptr It is almost used, but its function is more single. It is an exclusive smart pointer. Other smart pointers are not allowed to share their internal pointers. It is more like a native pointer (but it is safer and can release memory by itself). Assignment and copy operations are not allowed and can only be moved.
std::unique_ptr<int> ptr1(new int(0));
std::unique_ptr<int> ptr2 = ptr1; // Error, cannot copy
std::unique_ptr<int> ptr3 = std::move(ptr1); // Can move

stay C++11 In, there is no similar std::make_shared Initialization method, but in C++14 In, for std::unique_ptr Introduced std::make_unique Method.

#include <iostream>
#include <memory>

int main()
{
    std::unique_ptr<std::string> ptr1(new std::string("unique_ptr"));
    std::cout << "ptr1 is " << *ptr1 << std::endl;

    std::unique_ptr<std::string> ptr2 = std::make_unique<std::string>("make_unique init!");
    std::cout << "ptr2 is " << *ptr2 << std::endl;

    return 0;
}
/*
Output:
ptr1 is unique_ptr
ptr2 is make_unique init!
*/
Listed below unique_ptr Unique operation.
unique_ptr<T> u1 // Empty unique_ptr, which can point to an object of type T. u1 will use delete to release its pointer
unique_ptr<T, D> u2 // u2 uses a callable object of type D to release its pointer
unique_ptr<T, D> u(d) // Empty unique_ptr, point to the object of type T, and replace delete with object D of type D
u = nullptr // Release the object pointed to by u and leave u empty
u.release() // U relinquishes control of the pointer, returns the pointer, and sets u to null
u.reset() // Release the object pointed to by u
u.reset(q) // If the built-in pointer q is provided, the other u points to this object; Otherwise, set u to null
u.reset(nullptr)   

Although we can't copy or assign unique_ptr, but the ownership of the pointer can be changed from a (non const) unique by calling release or reset_ PTR transfer to another unique_ptr

unique_ptr<string> p1(new string("Stegosaurus"));
// Transfer ownership from pl (to string Stegosaurus) to p2 
unique_ptr<string> p2(p1, release()); // release sets p1 to null 
unique_ptr<string> p3(new string("Trex"));

// Transfer ownership from p3 to p2
p2.reset(p3.release()); // reset frees the memory that p2 originally pointed to

Calling release will cut off unique_ The relationship between PTR and the objects it originally managed. If we do not use another smart pointer to save the pointer returned by release, our program will be responsible for the release of resources:

p2.release(); // Error: p2 will not free memory, and we lost the pointer
auto p = p2.release(); // Correct, but we must remember delete(p)
delete(p);

Pass unique_ptr parameter and return unique_ptr

Cannot copy unique_ There is an exception to the rule of PTR: we can copy or assign a unique to be destroyed_ ptr. The most common example is to return a unique from a function_ ptr;

unique_ptr<int> clone (int p) 
{
	unique_ptr<int> ret(new int (p));
	// ...
    return ret;
}

For the above code, the compiler knows that the object to be returned will be destroyed. In this case, the compiler performs a special "copy", which is described in section 13.6.2 (page 473) of C++ Primer.

Trade off between performance and security

Although using smart pointers can solve the problem of memory leakage, it also pays a certain price. With shared_ptr example:

shared_ptr is twice the size of the original pointer, because it has an original pointer to the resource and a pointer to the reference count.

Memory for reference counts must be allocated dynamically. Although you can use make at all_ Shared () to avoid, but there are some cases where make cannot be used_ shared().

Increasing and decreasing the reference count must be atomic because read and write operations may occur simultaneously in different threads. For example, in a thread, there is a shared that points to a resource_ PTR may have called destructor (so the reference count of the resource pointed to is minus one). At the same time, in the other line, it points to a shared object of the same object_ The PTR may have performed a copy operation (therefore, the reference count is incremented by one). Atomic operations are generally slower than non atomic operations. But for thread safety, we have to do so, which brings unnecessary trouble to the single thread environment.

Keywords: C C++ pointer

Added by Canadiengland on Fri, 03 Sep 2021 08:35:43 +0300