gogoWebsite

C++11 Dynamic Memory Management

Updated to 2 days ago

I have to use smart pointers in my work recently. I have studied it again. I searched for a lot of information online and added the content I reprinted before. I sorted it out myself to take the essence. There are some examples I think of, and I mainly prefer to use it.

There are also some text descriptions cited from other articles (the descriptions are clear), but there are too many sources that are difficult to find, so I am sorry.


During the process of running the program, we often experience segfaults and continuous memory increase, which are problems in C++ explicit memory management. They are mainly summarized into the following points:

1. Wild pointer: Some memory units have been released, but the previous pointer to it is still in use.

2. Repeat release: The program tries to free the memory unit that has been released.

3. Memory Leak: No memory unit that is no longer used is released.

4. Buffer overflow: array crosses boundary.

5. Unpaired new[]/delete

In response to the above 1 to 3 problems, the C++ standard provides smart pointers to solve them.

Smart pointers are classes (templates) implemented based on the RAII (Resource Acquisition Is Initialization) mechanism, and have the behavior of pointers (overloading operator* and operator-> operators). When an object is created, it is initialized; after leaving its scope, the resource is released by automatically calling the destructor.

In C++98, smart pointers are implemented through a template type "auto_ptr". The auto_ptr object points to the dynamic memory created by new by initializing. When the life cycle of the auto_ptr object ends, its destructor will automatically release the dynamic memory owned by the auto_ptr object. Even if an exception occurs, dynamic memory can be released through the exception stack expansion process. But it has some disadvantages:

  • The target object for assignment and copy operations will first release the object it originally owns.
{
    auto_ptr<int> ap1(new int(100));
    auto_ptr<int> ap2(ap1);
    // ap1 == nullptr;
}        

auto_ptr cannot be used in stl containers, because the type required by stl containers must beValue semantic type. That is, two objects are equal after copying or assignment (ap1 == ap2)

  • Delete is called when auto_ptr destructuring, so you cannot use auto_ptr to manage array pointers.
~auto_ptr() { delete _M_ptr; }

Therefore, in the C++11 standard, smart pointers such as unique_ptr, shared_ptr and weak_ptr are used instead to manage dynamic memory. auto_ptr is left behind for compatibility with previous code and is not recommended.


Header file

When using these three smart pointers (unique_ptr, shared_ptr and weak_ptr), they need to include header files: <memory>
The namespace is: std


unique_ptr

concept:

  • unique_ptr "unique" has the object it refers to, and at the same time there can only be one unique_ptr pointing to the given object (std::move() is implemented by prohibiting copy semantics and only moving semantics).
  • The life cycle of the unique_ptr pointer itself: starts from when the unique_ptr pointer is created until it leaves the scope. When leaving scope, if it points to an object, the object it refers to is destroyed (the delete operator is used by default, and the user can specify other operations).
  • The relationship between unique_ptr pointer and the object it points to: During the life cycle of the smart pointer, the object pointed to by the smart pointer can be changed, such as when creating a smart pointer, it is specified by the constructor, it is re-specified by the reset method, it is released through the release method, and it is transferred through the moving semantics.

Basic usage:

#include <iostream>
 #include <memory>
 #include <vector>

 using namespace std;

 struct Foo {
     Foo() {}
     ~Foo() {}
     void Print() { cout << "Foo" << endl; }
 };

 int main(void)
 {
     Foo* p1 = new Foo();
     unique_ptr<Foo> up1; // up1==nullptr
 // up1 = p1; // Compile error, such assignment is not supported
     (p1); // Replace the management object and release the previously managed object
     p1 = nullptr;

 // unique_ptr<Foo> up2(up1); // Compile error, this construction is not supported
     unique_ptr<Foo> up2(std::move(up1)); // Up1 ownership is transferred to up2.  up1==nullptr

     (up2); // Exchange pointers of up2 and up1 management objects.  up2==nullptr

     if (up1) { // up1 != nullptr
         up1->Print(); // unique_ptr is overloaded->
         (*up1).Print(); // unique_ptr overloaded*
     }
 // up2->Print(); // Error up2 == nullptr, you must first judge before calling

     p1 = (); // get() returns the pointer of the managed object, up1 continues to hold its management rights
     p1 = (); // release() returns the pointer to the management object and releases the management rights, up1==nullptr
     delete p1;

     unique_ptr<Foo> up3(new Foo());
     (); // Display the memory of the release management object, you can also do this: up = nullptr;
     vector<unique_ptr<Foo>> v;
     unique_ptr<Foo> up4(new Foo());
 // v.push_back(up4); // Compile error, such copying is not supported
     v.push_back(std::move(up4); // Up4 can only give up ownership of it and transfer ownership to the container through std::move()

     return 0;
 }


Application scenarios:

  • As long as the unique_ptr smart pointer is created successfully, its destruction will be called to ensure the release of dynamic resources - avoid memory leakage.
  • Pass unique_ptr as a reference parameter to other routines, and don't worry that the pointer will be copied or accidentally released in the routine.


shared_ptr

concept:

  • shared_ptr is implemented based on the "reference counting" model. Multiple shared_ptr objects can have the same dynamic object and maintain a shared reference count. When the last shared_ptr pointing to the object is destroyed or reset, the object it refers to will be automatically released and dynamic resources will be recycled.
  • When destroying this object, use the default delete/delete[] expression, or a custom deleter passed in when constructing shared_ptr to achieve personalized resource release actions.

Basic usage

#include <iostream>
 #include <memory>
 #include <vector>
 #include <>

 using namespace std;

 struct Foo {
     int v;
 };

 int main(void)
 {
     shared_ptr<Foo> sp1(new Foo{10});
     cout << () << endl; // 1 When currently shared_ptr only has Foo management rights, returns true, otherwise returns false
     cout << sp1.use_count() << endl; // 1 Returns the reference count of the current object

     shared_ptr<Foo> sp2(sp1);
     assert(sp1->v == sp2->v); // sp1 and sp2 jointly own Foo object
     cout << () << endl; // 0 false
     cout << sp2.use_count() << endl; // 2
   
     (); // Release the management rights of Foo, and reduce the reference count by 1
     assert(sp1 == nullptr); // sp1 is empty
     cout << () << endl; // 0 will not throw an exception
     cout << sp1.use_count() << endl; // 0 will not throw an exception
     cout << () << endl; // 0 No exception runs
 // cout << sp1->v << endl; // When executing error sp1 is nullptr, both operator* and operator-> will cause undefined behavior

     cout << () << endl; // 1 true
     (sp2); // Switch management rights between sp1 and sp2 and reference count
     assert(sp2 == nullptr);
     cout << (*sp1).v << endl; // 10 Same as sp1->v

     vector<shared_ptr<Foo>> vec;
     vec.push_back(sp1);
     cout << sp1.use_count() << endl; // 2

     return 0;
 // The vector first destructs the object reference count stored in it, and the object reference count is reduced by 1, but not 0, and the object is not released
 // Destruction after sp1, the reference count is reduced by 1, and becomes 0, freeing the memory resource of the referred object
 }
 
 

Main methods

T& operator*() const;
T* operator->() const;
T* get() const;
bool unique() const;
long use_count() const;
void swap(shared_ptr<T>& b);


Application scenarios:

  1. In a map (unordered_map), multiple keys index a value, using shared_ptr.
    #include <iostream>
     #include <memory>
     #include <map>
     #include <cstdint>
    
     using namespace std;
    
     Class SessionNode {
     public:
         SessionNode() {}
         virtual ~SessionNode() {}
     };
     typedef std::shared_ptr<SessionNode> SessionNodeSP;
    
     int main(void)
     {
         map<uint64_t, SessionNodeSP> map;
         uint64_t imsi = 4600000;
         uint32_t tmsi = 0x12345;
         {
             SessionNodeSP sp(new SessionNode());
             map[imsi] = sp;
             map[tmsi] = sp;
             cout << sp.use_count() << endl; // 3
         } // sp Destroy, the reference count is reduced by 1, and becomes 2
    
         (tmsi); // use_count() is 1
         (imsi); // use_count() is 0, freeing the memory of managed objects
        
         return 0;
     }
  2. Multiple map indexes the same value
    #include <iostream>
     #include <memory>
     #include <map>
     #include <cstdint>
    
     using namespace std;
    
     struct Node {
         uint64_t imsi;
         uint32_t tmsi;
     };
     typedef std::shared_ptr<Node> NodeSP;
    
     class Session {
     public:
         Session() {}
         ~Session() {}
         NodeSP GetNode(uint64_t imsi, uint32_t tmsi) {
             NodeSP sp(new Node{imsi, tmsi});
             imsi_map_[imsi] = sp;
             tmsi_map_[tmsi] = sp;
             return sp;
         }
         void EraseNode(NodeSP& sp) {
             if (sp == nullptr)
                 return;
             imsi_map_.erase(sp->imsi);
             imsi_map_.erase(sp->tmsi);
         }
     private:
         map<uint64_t, NodeSP> imsi_map_;
         map<uint32_t, NodeSP> tmsi_map_;
     };
    
     int main(void)
     {
         Session ses;
         uint64_t imsi = 4600000;
         uint32_t tmsi = 0x12345;
        
         NodeSP sp;
         sp = (imsi, tmsi);
         cout << sp.use_count() << endl; // 3
    
         // ... do something with sp
    
         (sp);
         cout << sp.use_count() << endl; // 1
        
         (); // Actively release the memory of managed objects
    
         return 0;
     }
  3. Prevent naked pointers from being deleted
    #include <iostream>
     #include <memory>
    
     class Cdr {
     public:
         Cdr() {}
     protected:
         virtual ~Cdr() {}
     };
    
     class SignalCdr : public Cdr {
     public:
         SignalCdr() {}
         virtual ~SignalCdr() {}
     };
    
     typedef std::shared_ptr<Cdr> CdrSP;
     CdrSP CreateSignalCdr()
     {
         CdrSP sp(new SignalCdr());
         return sp;
     };
    
     int main(void)
     {
         CdrSP sp_cdr = CreateSignalCdr();
         SignalCdr* p_signal_cdr = reinterpret_cast<SignalCdr*>(sp_cdr.get());
         // ... do something
     // delete p_signal_cdr; // Execution error, ~Cdr() is protected
    
         return 0;
     }
  4. Use weak_ptr when causing a circular reference.
    #include <iostream>
     #include <memory>
    
     using namespace std;
     struct Husband;
     struct Wife;
     typedef std::shared_ptr<Husband> HusbandSP;
     typedef std::shared_ptr<Wife> WifeSP;
    
     struct Wife {
         ~Wife() { cout<< "wife distroy" << endl; }
         HusbandSP sp_hb;
     };
     struct Husband {
         ~Husband() { cout<< "husband distroy" << endl; }
         WifeSP sp_wf;
     };
    
     int main(void)
     {
         {
             HusbandSP husband(new Husband());
             WifeSP wife(new Wife());
             husband->sp_wf = wife;
             wife->sp_hb = husband;
         } // husband and wife refer to each other, and the reference count is 1 when leaving the scope, causing memory leakage
    
         return 0;
     }

weak_ptr

concept

  • weak_ptr is a smart pointer introduced in conjunction with shared_ptr. It can only be constructed through shared_ptr or weak_ptr.
  • Weak_ptr is the "observer" of shared_ptr and does not modify the reference count of the objects managed by shared_ptr. When shared_ptr is destroyed, weak_ptr will be set to empty. Therefore, the advantage of using weak_ptr over the underlying pointer is that it can know whether the pointed object is valid or not.
  • weak_ptr does not have the behavior of a normal pointer, because there is no overloading of operator* and ->. Therefore, when the object observed by weak_ptr exists and needs to be modified, it needs to be promoted to shared_ptr to operate.

Basic usage

#include <iostream>
 #include <memory>

 using namespace std;

 struct Cdr{
     int v;
 };
 typedef std::shared_ptr<Cdr> CdrSP;
 typedef std::weak_ptr<Cdr> CdrWP;

 void UpdateCdr(CdrWP wp)
 {
     if (() == false) { // Check whether the managed object is deleted, true deleted, false has not been deleted; faster than use_count()==1
         CdrSP sp = (); // Promote to strong citation
         if (sp != nullptr) { // If the promotion fails, shared_ptr is nullptr, this example will not fail
             sp->v *= 2;
             cout << sp.use_count() << endl; // The reference count is 2 at this time
         }
     } // sp delete, reference count minus 1
     (); // Show that the ownership is released, or the scope will be automatically released
 }

 int main(void)
 {
     CdrSP sp(new Cdr{10});

     UpdateCdr(sp); // Operation on sp

     cout << sp->v << endl; // 20

     return 0;
 }

Main methods

long use_count() const;
bool expired() const;
std::shared_ptr<T> lock() const;

Application scenarios

  1. For example, the basic usage is to observe the shared_ptr managed objects through weak_ptr. If it is effective, it is promoted to shared_ptr for operation.
  2. Storage of weak references in map
    #include <iostream>
     #include <memory>
     #include <map>
    
     using namespace std;
    
     struct Cdr {
         int v;
     };
     typedef std::shared_ptr<Cdr> CdrSP;
     typedef std::weak_ptr<Cdr> CdrWP;
    
     class Cache {
     public:
         Cache() {}
         ~Cache() {}
         void UpdateIndex(CdrSP& sp) {
             if (sp != nullptr && sp->v > 0)
                 cdr_map_[sp->v] = sp;
         }
     private:
         map<int, CdrWP> cdr_map_;
     };
    
     int main(void)
     {
         Cache cache;
         CdrSP sp_cdr(new Cdr{1});
    
         (sp_cdr);
         cout << sp_cdr.use_count() << endl; // 1
        
         return 0;
     // sp_cdr is destroyed, the reference count is 1, and the memory of the management object is released
     // cache is destroyed, because the stored in the map is weak_ptr and weap_ptr is empty, so no secondary release will be generated
     }

bad_weak_ptr exception capture

When shared_ptr is constructed through the weak_ptr parameter, and weak_ptr points to an object that has been deleted, a std::bad_weak_ptr exception is thrown.

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    shared_ptr<int> sp1(new int(1));
    weak_ptr<int> wp(sp1);
    ();
    try {
        shared_ptr<int> sp2(wp);
    } catch (const std::bad_weak_ptr& e) {
        cout << () << endl;    // "std::bad_weak_ptr"
    }
}

Create shared_ptr from this

Sometimes, you need to get shared_ptr from this, that is, you want your class to be managed by shared_ptr, and you need to convert "self" into shared_ptr method.

#include <iostream>
#include <memroy>

using namespace std;

class Foo;
typedef std::shared_ptr<Foo> FooSP;

void DoSomething(const FooSP& sp) {
    cout << sp.use_count() << endl; // 2
}

class Foo : public std::enable_shared_from_this<Foo> {
public:
    Foo() {}
    ~Foo() {}
    void Do() {
        DoSomething(shared_from_this());
    }
};

int main(void)
{
    FooSP sp(new Foo());
    cout << sp.use_count() << endl; // 1

    sp->Do();

    return 0;
}


Summarize:

  • In unique_ptr/shared_ptr, get() exposes the underlying pointer to be compatible with old programs and is generally not recommended because it is difficult to ensure that other routines will do something with this pointer, such as delete/delete[].
  • When passed as shared_ptr and weak_pt as parameters, reference pass is used to reduce overhead.
  • weka_ptr does not overload operator==, so it cannot be compared. shared_ptr can be compared, and the underlying pointer is compared.
  • auto_ptr, unique_ptr, shared_ptr, cannot be used like this
    {
        int* p = new int(100);
        auto_ptr<int> ap1(p);
        auto_ptr<int> ap2(p);
    }
    When leaving scope, both ap1 and ap2 try to delete p, causing double free.
  • Using smart pointers does not require manual processing of reference counts and calling delete to free resources. But be clear when resources will be released.
    For example: use a container to store shared_ptr and release resources when deleted from the container, then other routines only update their resources when using shared_ptr;
    If the resource is not released when deleting shared_ptr from the container, it should be shared by another shared_ptr; at this time, it is more appropriate to store weak_ptr in the container.


English website:

/w/cpp/memory

Chinese website (google machine translation):

/w/cpp/memory