Explain the smart pointer in C + +

preface

Four smart pointers in C + +: auto_ptr, unique_ptr,shared_ptr, weak_ptr, of which the last three are supported by C++11, and the first has been abandoned by C++11.

Introduction to C++11 intelligent pointer

Smart pointer is mainly used to manage the memory allocated on the heap. It encapsulates 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. The most commonly used smart pointer type in C++ 11 is shared_ptr, which uses the reference counting method to record how many smart pointers the current memory resources are referenced by. Memory for this reference count is allocated on the heap. When a new one is added, the reference count increases by 1, and when it expires, the reference count decreases by 1. Only when the reference count is 0, the smart pointer will automatically release the referenced memory resources. Yes, shared_ When initializing PTR, a normal pointer cannot be directly assigned to a smart pointer, because one is a pointer and the other is a class. You can use make_ The shared function or pass in a normal pointer through the constructor. And you can get ordinary pointers through the get function.

Why use smart pointers

The function of smart pointer is to manage a pointer, because there are the following situations: the requested space is forgotten to be released at the end of the function, resulting in memory leakage. The problem of automatically releasing the pointer of a class when the pointer of the class is called beyond the scope of the function is largely avoided. Therefore, the function principle of smart pointer is to automatically release the memory space at the end of the function, and there is no need to release the memory space manually.

auto_ptr

(the scheme of C++98 and C++11 have been abandoned) adopt the ownership mode.

auto_ptr<string> p1 (new string ("I reigned lonely as a cloud.")); 
auto_ptr<string> p2; 
p2 = p1; //auto_ptr will not report an error

No error will be reported at this time. p2 deprives p1 of ownership, but an error will be reported when accessing p1 when the program is running. So Auto_ The disadvantage of PTR is that there is a potential memory corruption problem!

unique_ptr

(replace auto_ptr) unique_ptr implements the concept of exclusive ownership or strict ownership, ensuring that only one smart pointer can point to the object at the same time. It is particularly useful for avoiding resource leakage (such as "forgetting to call delete because of an exception after creating an object with new").

Using the ownership model, or the above example

unique_ptr<string> p3 (new string ("auto"));   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;//An error will be reported at this time!!

The compiler considers p4=p3 illegal, which avoids the problem that p3 no longer points to valid data. A compile time error occurs when trying to copy p3, and auto_ptr can bury the hidden trouble of error in the run-time by passing the compile time. Therefore, unique_ptr ratio auto_ptr is safer.

In addition, unique_ptr is also smarter: when a program tries to put a unique_ When PTR is assigned to another, if the source is unique_ptr is a temporary right value, which is allowed by the compiler; If the source is unique_ptr will exist for some time and the compiler will prohibit it, such as:

unique_ptr<string> pu1(new string ("hello world")); 
unique_ptr<string> pu2; 
pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3; 
pu3 = unique_ptr<string>(new string ("You"));   // #2 allow

Which #1 leaves the hanging unique_ptr(pu1), which may cause harm. And #2 will not leave the hanging unique_ptr, because it calls unique_ptr constructor. The temporary object created by this constructor will be destroyed after its ownership is transferred to pu3. This casual behavior shows that unique_ptr is better than auto, which allows two assignments_ ptr .

Note: if you really want to perform operations similar to #1, you can assign a new value to this pointer if you want to safely reuse it. C + + has a standard library function std::move(), which allows you to put a unique_ptr is assigned to another. Although it is still possible to call the original pointer (the call will crash) after the transfer of ownership. But this syntax can emphasize that you are transferring ownership, so that you can clearly know what you are doing, so as not to call the original pointer indiscriminately.

(additional: the boost::scoped_ptr of the boost library is also an exclusive smart pointer, but it does not allow the transfer of ownership. It is only responsible for one resource from beginning to end. It is more secure and cautious, but the scope of application is also narrower.)

For example:

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

shared_ptr

shared_ptr implements the concept of shared ownership. Multiple smart pointers can point to the same object, which and its related resources will be released when the "last reference is destroyed". From the name share, we can see that resources can be shared by multiple pointers. It uses the counting mechanism to indicate that resources are shared by several pointers. You can use through the member function_ Count() to view the number of owners of the resource. In addition to being constructed through new, you can also pass in auto_ptr, unique_ptr,weak_ptr. When we call release(), the current pointer releases the ownership of the resource and the count decreases by one. When the count equals 0, the resource is released.

shared_ptr is to solve Auto_ The limitation of PTR in object ownership (auto_ptr is exclusive) provides a smart pointer that can share ownership in the mechanism of using reference counting.

Member function:

use_count returns the number of references counted

unique returns whether it is exclusive ownership (use_count is 1)

swap exchange two shared_ptr object (i.e. the object owned by the exchange)

reset abandons the ownership of internal objects or changes of ownership objects, which will reduce the reference count of the original objects

get returns the internal object (pointer). Since the () method has been overloaded, it is the same as using the object directly as

shared_ptr<int> sp(new int(1)); 

SP is equivalent to sp.get().

share_ A simple example of PTR:

int main()
{
	string *s1 = new string("s1");

	shared_ptr<string> ps1(s1);
	shared_ptr<string> ps2;
	ps2 = ps1;

	cout << ps1.use_count()<<endl;	//2
	cout<<ps2.use_count()<<endl;	//2
	cout << ps1.unique()<<endl;	//0

	string *s3 = new string("s3");
	shared_ptr<string> ps3(s3);

	cout << (ps1.get()) << endl;	//033AEB48
	cout << ps3.get() << endl;	//033B2C50
	swap(ps1, ps3);	//Exchange owned objects
	cout << (ps1.get())<<endl;	//033B2C50
	cout << ps3.get() << endl;	//033AEB48

	cout << ps1.use_count()<<endl;	//1
	cout << ps2.use_count() << endl;	//2
	ps2 = ps1;
	cout << ps1.use_count()<<endl;	//2
	cout << ps2.use_count() << endl;	//2
	ps1.reset();	//Give up ownership of ps1 and reduce the reference count
	cout << ps1.use_count()<<endl;	//0
	cout << ps2.use_count()<<endl;	//1
}

weak_ptr

share_ Although PTR is already very easy to use, it has a little share_ptr smart pointer still has memory leakage. When two objects use a shared pointer to each other_ PTR member variables point to each other, which will cause circular reference and invalidate the reference count, resulting in memory leakage.

weak_ptr is a smart pointer that does not control the life cycle of an object. It points to a shared_ptr management object The memory management of this object is the strongly referenced shared_ptr, weak_ptr only provides a means of access to management objects. weak_ The purpose of PTR design is to cooperate with shared_ptr introduces a smart pointer to assist shared_ptr works, it can only be from a shared_ Weptr or another_ PTR object construction, its construction and deconstruction will not cause the increase or decrease of reference count. weak_ptr is used to solve shared_ Deadlock problem when PTR refers to each other, if two shared_ptr refers to each other, then the reference count of these two pointers can never drop to 0, and the resources will never be released. It is a weak reference to an object and will not increase the reference count of the object and shared_ PTRs can be transformed into each other_ PTR can be directly assigned to it, and it can get shared by calling lock function_ ptr.

class B;	//statement
class A
{
public:
	shared_ptr<B> pb_;
	~A()
	{
		cout << "A delete\n";
	}
};

class B
{
public:
	shared_ptr<A> pa_;
	~B()
	{
		cout << "B delete\n";
	}
};

void fun()
{
	shared_ptr<B> pb(new B());
	shared_ptr<A> pa(new A());
	cout << pb.use_count() << endl;	//1
	cout << pa.use_count() << endl;	//1
	pb->pa_ = pa;
	pa->pb_ = pb;
	cout << pb.use_count() << endl;	//2
	cout << pa.use_count() << endl;	//2
}

int main()
{
	fun();
	return 0;
}

It can be seen that pa and Pb in the fun function refer to each other, and the reference count of the two resources is 2. When you want to jump out of the function, the reference count of the two resources will be reduced by 1 when the smart pointer pa and Pb are destructed, but the reference count of the two is still 1. As a result, the resources are not released when you jump out of the function (the destructors of a and B are not called). The running result does not output the contents of the destructor, resulting in memory leakage. If you change one of them to weak_ptr is OK. Let's take shared in class A_ ptr pb_, Change to weak_ptr pb_ , The operation results are as follows:

1
1
1
2
B delete
A delete

In this way, the reference of resource B is only 1 at the beginning. When pb destructs, the count of B becomes 0 and B is released. When B releases, it will also reduce the count of A by 1. At the same time, when pa destructs, it will reduce the count of A by 1, then the count of A is 0 and A is released.

Note: we can't pass weak_ptr directly accesses the method of the object. For example, there is a method print () in the B object. We can't access it in this way. Pa - > pb_ -> print(), because PB_ It's a weak_ptr should be converted into shared first_ PTR, such as:

shared_ptr<B> p = pa->pb_.lock();
p->print();

weak_ptr does not overload * and - > but you can use lock to get an available shared_ptr object Attention, weak_ The validity of PTR should be checked before use

expired is used to detect whether the managed object has been released. If it has been released, it returns true; Otherwise, false is returned

lock is used to obtain the shared_ptr of the managed object If expired is true, an empty shared is returned_ ptr; Otherwise, a shared is returned_ PTR, whose internal object points to weak_ptr is the same

use_count returns and shared_ Reference count of PTR shared objects

reset will break_ PTR is set to null

weak_ptr supports copying or assignment, but does not affect the corresponding shared_ The count of PTR internal objects

share_ptr and weak_ The core implementation of PTR

As a weak reference pointer, the implementation of weakptr depends on the counter class and share of counter_ The assignment and construction of PTR, so first put counter and share_ Simple implementation of PTR

Simple implementation of Counter

class Counter
{
public:
    Counter() : s(0), w(0){};
    int s;	//share_ Reference count of PTR
    int w;	//weak_ Reference count of PTR
};

The purpose of the counter object is to apply for a block of memory to store the reference cardinality. s is share_ Reference count of PTR, W is weak_ The reference count of PTR. When w is 0, the counter object is deleted.

share_ Simple implementation of PTR

template <class T>
class WeakPtr; //To use weak_ptr lock() to generate share_ For PTR, copy construction is required

template <class T>
class SharePtr
{
public:
    SharePtr(T *p = 0) : _ptr(p)
    {
        cnt = new Counter();
        if (p)
            cnt->s = 1;
        cout << "in construct " << cnt->s << endl;
    }
    ~SharePtr()
    {
        release();
    }

    SharePtr(SharePtr<T> const &s)
    {
        cout << "in copy con" << endl;
        _ptr = s._ptr;
        (s.cnt)->s++;
        cout << "copy construct" << (s.cnt)->s << endl;
        cnt = s.cnt;
    }
    SharePtr(WeakPtr<T> const &w) //To use weak_ptr lock() to generate share_ For PTR, copy construction is required
    {
        cout << "in w copy con " << endl;
        _ptr = w._ptr;
        (w.cnt)->s++;
        cout << "copy w  construct" << (w.cnt)->s << endl;
        cnt = w.cnt;
    }
    SharePtr<T> &operator=(SharePtr<T> &s)
    {
        if (this != &s)
        {
            release();
            (s.cnt)->s++;
            cout << "assign construct " << (s.cnt)->s << endl;
            cnt = s.cnt;
            _ptr = s._ptr;
        }
        return *this;
    }
    T &operator*()
    {
        return *_ptr;
    }
    T *operator->()
    {
        return _ptr;
    }
    friend class WeakPtr<T>; //Easy break_ PTR and share_ptr setting reference count and assignment

protected:
    void release()
    {
        cnt->s--;
        cout << "release " << cnt->s << endl;
        if (cnt->s < 1)
        {
            delete _ptr;
            if (cnt->w < 1)
            {
                delete cnt;
                cnt = NULL;
            }
        }
    }

private:
    T *_ptr;
    Counter *cnt;
};

share_ The function interfaces given by ptr are: construction, copy construction, assignment, dereference, and delete when the reference count is 0 through release_ ptr and cnt memory.

weak_ Simple implementation of PTR

template <class T>
class WeakPtr
{
public: //The default structure and copy structure are given. The copy structure cannot be constructed from the original pointer
    WeakPtr()
    {
        _ptr = 0;
        cnt = 0;
    }
    WeakPtr(SharePtr<T> &s) : _ptr(s._ptr), cnt(s.cnt)
    {
        cout << "w con s" << endl;
        cnt->w++;
    }
    WeakPtr(WeakPtr<T> &w) : _ptr(w._ptr), cnt(w.cnt)
    {
        cnt->w++;
    }
    ~WeakPtr()
    {
        release();
    }
    WeakPtr<T> &operator=(WeakPtr<T> &w)
    {
        if (this != &w)
        {
            release();
            cnt = w.cnt;
            cnt->w++;
            _ptr = w._ptr;
        }
        return *this;
    }
    WeakPtr<T> &operator=(SharePtr<T> &s)
    {
        cout << "w = s" << endl;
        release();
        cnt = s.cnt;
        cnt->w++;
        _ptr = s._ptr;
        return *this;
    }
    SharePtr<T> lock()
    {
        return SharePtr<T>(*this);
    }
    bool expired()
    {
        if (cnt)
        {
            if (cnt->s > 0)
            {
                cout << "empty" << cnt->s << endl;
                return false;
            }
        }
        return true;
    }
    friend class SharePtr<T>; //Easy break_ PTR and share_ptr setting reference count and assignment
    
protected:
    void release()
    {
        if (cnt)
        {
            cnt->w--;
            cout << "weakptr release" << cnt->w << endl;
            if (cnt->w < 1 && cnt->s < 1)
            {
                //delete cnt;
                cnt = NULL;
            }
        }
    }

private:
    T *_ptr;
    Counter *cnt;
};

weak_ptr is generally through share_ptr, check whether the original pointer is null through the expired function, and lock to convert it into share_ptr.

 

Pay attention to the original official account: "code has its way", and talk about the development of the whole game stack together!

1. Official account reply: [tutorial] get a full set of development tutorials for zero basic game client + server.

2. Official account reply: [actual combat] obtain enterprise level actual combat projects.

Tags: C++ pointer

Posted by kabucek1 on Mon, 23 May 2022 07:09:43 +0300