The introduction

Recently, while typing on a C++ project, there was a serious and self-defeating memory leak, as shown below:

Query the method of memory leakage

What is a memory leak

The memory leak is explained in Wikipedia as follows:

In computer science, a memory leak is the failure of a program, through negligence or error, to free memory that is no longer in use. A memory leak is not the physical disappearance of memory, but rather the loss of control of memory before it is released due to a design error after an application allocates the memory.

In C++, the main reason for memory leakage is that the program does not release the unused memory space in time after applying for memory (malloc(), new), or even destroys the pointer so that the memory space cannot be freed at all.

Know the causes of memory leaks can know how to deal with memory leaks, that is: unused memory space remember to release, do not release to keep the New Year wow!

Memory leaks can have serious consequences:

  • After the program runs, it takes up more memory over time, and finally crashes when there is no memory available.
  • The program consumes a large amount of memory, resulting in other programs can not be used properly;
  • Programs consume a lot of memory, leading consumers to choose someone else’s program over yours;
  • Programmers who often make memory leak bugs are kicked out of the company and impoverished.

How do I know if my program has a memory leak?

Based on the causes of memory leaks and their dire consequences, we can tell if there is a memory leak in an application by its main manifestation: the memory usage of the application continues to rise slowly after a long time, when there is no memory requirement in your logic.

How do you locate the leak?

  1. We can review our own code and use the “find” function to query new and delete to see if the memory is allocated and released in pairs. This will allow you to quickly detect some memory leaks with relatively simple logic.

  2. If memory leaks still occur, you can determine by recording whether the number of objects applied and released is consistent. Append a static int count to the class; Call count++ in the constructor; Execute count–; in the destructor By destructing all classes before the end of the program, and then printing a static variable to see if the value of count is 0. If it is 0, then the problem is not there. If it is not 0, then the object of the type is not completely freed.

  3. Check whether the space allocated by the class is fully released, especially if there is an inherited parent class. Check whether the destructor of the parent class is called in the child class, possibly because there is no memory allocated by the parent class.

  4. For the temporary space applied in the function, check carefully to see if there is any place that jumps out of the function in advance and does not free memory.

Smart pointer to STL

To reduce memory leaks, smart Pointers are used in the STL to reduce leaks. There are generally four kinds of smart Pointers in STL:

Pointer to the class support note
unique_ptr C++ 11 A smart pointer with unique object ownership semantics
shared_ptr C++ 11 Smart Pointers to shared object ownership semantics
weak_ptr C++ 11 Weak references to objects managed by STD ::shared_ptr
auto_ptr C++ 17 removed Smart Pointers with strict object ownership semantics

Since auto_ptr has been removed from C++ 17, it is best for future-oriented programmers to reduce the frequency of its use in their code, so I won’t look at it here. And because weak_ptr is a weak reference of shared_ptr, the main pointer can only be divided into two unique_ptr and shared_ptr.

STD ::unique_ptr is a smart pointer that holds and manages another object through a pointer and releases that object when unique_ptr leaves scope. Release an object with the associated remover when one of the following occurs:

  • The managed unique_ptr object was destroyed
  • Assign another pointer to the managed unique_ptr via operator= or reset().

STD :: shareD_ptr is a smart pointer that maintains shared ownership of objects through Pointers. Multiple shareD_ptr objects can own the same object. Destroy an object and unallocate its memory when one of the following occurs:

  • The last remaining shared_ptr of the possessive object is destroyed;
  • The last remaining possession object’s shared_ptr is assigned to another pointer via operator= or reset().

unique_ptr

This is an exclusive pointer object. Only one pointer can hold a resource at any time. When unique_ptr leaves scope, the contents of the pointer are released.

create

unique_ptr<int> uptr( new int );
unique_ptr<int[ ]> uptr( new int[5]);// Declare that it can be initialized with a pointer, or declared as a null pointer to an object of type T
shared_ptr<T> sp;
unique_ptr<T> up;
// Assign, return a smart pointer to a dynamically allocated object of type T, and initialize the object with args
make_shared<T>(args);
make_unique<T>(args);     // note that make_unique comes after C++14
Return true if it refers to an object, false otherwise
p;
/ / reference solution
*p;
// Get the pointer it saved
p.get();
// Swap Pointers
swap(p,q);
p.swap(q);

/ / release the usage ()
 //release() returns the pointer pointed to by the smart pointer, only responsible for transferring control, not freeing memory
 unique_ptr<int> q(p.release()) // P loses control to Q, and p points to nullPtr
 // So if you use it alone:
 p.release()
 // Then p loses control and the original memory is not freed
 // results in //reset() usage
 p.reset()     // Set p to nullptr,
 p = nullptr   // Same as the previous step
 p.reset(q)    // Note that q is a built-in pointer that causes P to free memory and p to point to the object

Copy the code

The class satisfies MoveConstructible and MoveAssignable requirements, but not CopyConstructible or CopyAssignable requirements. Therefore, you cannot use the = operation and copy constructor, only the move operation.

Demo

#include <iostream>
#include <vector>
#include <memory>
#include <cstdio>
#include <fstream>
#include <cassert>
#include <functional>

struct B {
  virtual void bar(a) { std: :cout << "B::bar\n"; }
  virtual ~B() = default;
};
struct D : B
{
    D() { std: :cout << "D::D\n";  }
    ~D() { std: :cout << "D::~D\n";  }
    void bar(a) override { std: :cout << "D::bar\n"; }};// Functions consuming unique_ptr can receive it as a value or as an rvalue reference
std: :unique_ptr<D> pass_through(std: :unique_ptr<D> p)
{
    p->bar();
    return p;
}

void close_file(std::FILE* fp) { std::fclose(fp); }

int main(a)
{
  std: :cout << "unique ownership semantics demo\n";
  {
      auto p = std::make_unique<D>(); // p is unique_ptr of D
      auto q = pass_through(std::move(p)); assert(! p);// now p does not hold anything and keeps the null pointer
      q->bar();   // Q owns D
  } // The ~D call is used for this

  std: :cout << "Runtime polymorphism demo\n";
  {
    std: :unique_ptr<B> p = std::make_unique<D>(); // p is unique_ptr of D
                                                  // as a pointer to the base class
    p->bar(); / / out

    std: :vector<std: :unique_ptr<B>> v;  // unique_ptr can be stored in containers
    v.push_back(std::make_unique<D>());
    v.push_back(std::move(p));
    v.emplace_back(new D);
    for(auto& p: v) p->bar(); / / out
  } // ~D called 3 times

  std: :cout << "Custom deleter demo\n";
  std::ofstream("demo.txt") < <'x'; // Prepare the file to read
  {
      std: :unique_ptr<std::FILE, void(*) (std::FILE*) > fp(std::fopen("demo.txt"."r"),
                                                           close_file);
      if(fp) // Fopen can fail to open; In this case, FP guarantees a null pointer
        std: :cout< < (char)std::fgetc(fp.get()) << '\n';
  } The fclose() call is used for this, but only if FILE* is not a null pointer
    // (fopen succeeded)

  std: :cout << "Custom lambda-expression deleter demo\n";
  {
    std: :unique_ptr<D, std::function<void(D*)>> p(new D, [](D* ptr)
        {
            std: :cout << "destroying from a custom deleter... \n";
            delete ptr;
        });  // p owns D
    p->bar();
  } // Call the lambda above and destroy D

  std: :cout << "Array form of unique_ptr demo\n";
  {
      std: :unique_ptr<D[]> p{new D[3]};
  } // Call ~D three times
}
Copy the code

Output result:

unique ownership semantics demo
D::D
D::bar
D::bar
D::~D
Runtime polymorphism demo
D::D
D::bar
D::D
D::D
D::bar
D::bar
D::bar
D::~D
D::~D
D::~D
Custom deleter demo
x
Custom lambda-expression deleter demo
D::D
D::bar
destroying from a custom deleter...
D::~D
Array form of unique_ptr demo
D::D
D::D
D::D
D::~D
D::~D
D::~D
Copy the code

shared_ptr

There are two ways to create shareD_ptr: use the make_shared macro to speed up the creation process. Because shared_Ptr actively allocates memory and holds reference counts,make_shared achieves creation in a more efficient way.


void main( )
{
 shared_ptr<int> sptr1( new int );
 shared_ptr<int> sptr2 = make_shared<int> (100);
}
Copy the code

destructor

Shared_ptr calls delete by default to release associated resources. If the user adopts a different destructor policy, he is free to specify the policy to construct the shared_ptr. In this scenario, shared_ptr points to a set of objects, but when out of scope, the default destructor calls DELETE to free the resource. In fact, we should call delete[] to destroy the array. A user can specify a generic release step by calling a function, such as a LAMda expression.

void main( )
{
 shared_ptr<Test> sptr1( new Test[5],
        [ ](Test* p) { delete[ ] p; }); }Copy the code

Do not use bare Pointers to create shareD_ptr. Otherwise, different groups may cause errors

void main( )
{
/ / error
 int* p = new int;
 shared_ptr<int> sptr1( p);   // count 1
 shared_ptr<int> sptr2( p );  // count 1

/ / right
 shared_ptr<int> sptr1( new int );  // count 1
 shared_ptr<int> sptr2 = sptr1;     // count 2
 shared_ptr<int> sptr3;           
 sptr3 =sptr1                       // count 3
}
Copy the code

A circular reference

Because Shared_ptr is a pointer to multiple points, circular references can occur, causing memory to remain unfreed beyond scope.

class B;
class A
{
public:
 A(  ) : m_sptrB(nullptr) {}; ~A( ) {cout<<" A is destroyed"<<endl;
 }
 shared_ptr<B> m_sptrB;
};
class B
{
public:
 B(  ) : m_sptrA(nullptr) {}; ~B( ) {cout<<" B is destroyed"<<endl;
 }
 shared_ptr<A> m_sptrA;
};
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
void main( )
{
 shared_ptr<B> sptrB( new B );  // sptB count 1
 shared_ptr<A> sptrA( new A );  // sptB count 1
 sptrB->m_sptrA = sptrA;    // sptB count 2
 sptrA->m_sptrB = sptrB;    // sptA count 2
}

// out of bounds
// sptA count 1
// sptB count 2
Copy the code

demo

#include <iostream> #include <memory> #include <thread> #include <chrono> #include <mutex> struct Base { Base() { std::cout << " Base::Base()\n"; } / / note: here the virtual destructors OK ~ Base () {STD: : cout < < "Base: : ~ Base (\ n"); }}; struct Derived: public Base { Derived() { std::cout << " Derived::Derived()\n"; } ~Derived() { std::cout << " Derived::~Derived()\n"; }}; void thr(std::shared_ptr<Base> p) { std::this_thread::sleep_for(std::chrono::seconds(1)); std::shared_ptr<Base> lp = p; Static STD ::mutex io_mutex; std::lock_guard<std::mutex> lk(io_mutex); std::cout << "local pointer in a thread:\n" << " lp.get() = " << lp.get() << ", lp.use_count() = " << lp.use_count() << '\n'; } } int main() { std::shared_ptr<Base> p = std::make_shared<Derived>(); std::cout << "Created a shared Derived (as a pointer to Base)\n" << " p.get() = " << p.get() << ", p.use_count() = " << p.use_count() << '\n'; std::thread t1(thr, p), t2(thr, p), t3(thr, p); p.reset(); STD ::cout << "Shared ownership between 3 threads and released\n" << "ownership from main:\n" <<" p.get() = " << p.get() << ", p.use_count() = " << p.use_count() << '\n'; t1.join(); t2.join(); t3.join(); std::cout << "All threads completed, the last one deleted Derived\n"; }Copy the code

Possible output results

Base::Base()
  Derived::Derived()
Created a shared Derived (as a pointer to Base)
  p.get() = 0xc99028, p.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
  p.get() = (nil), p.use_count() = 0
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 3
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 4
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 2
  Derived::~Derived()
  Base::~Base()
All threads completed, the last one deleted Derived
Copy the code

weak_ptr

STD :: Weak_ptr is an intelligent pointer that has non-owning (” weak “) references to objects managed by STD :: ShareD_Ptr. STD :: shareD_ptr must be converted before the referenced object can be accessed.

STD :: Weak_Ptr is used to express the concept of temporary ownership: When an object needs to be accessed only if it exists and can be deleted by others at any time, you can use STD :: Weak_ptr to track that object. When temporary ownership is required, it is converted to STD :: shareD_ptr, at which point, if the original STD ::shared_ptr is destroyed, the life of the object is extended until the temporary STD :: shared_Ptr is also destroyed.

Another use of STD :: Weak_ptr is to break circular references to objects managed by STD :: shareD_Ptr. If such a ring is isolated (such as an external shared pointer in an undirected ring), the shared_ptr reference count cannot reach zero and memory leaks. Can avoid this by making one of the Pointers in the ring weak.

create

void main( )
{
 shared_ptr<Test> sptr( new Test );   // strong reference 1
 weak_ptr<Test> wptr( sptr );         // Strong reference 1 weak reference 1
 weak_ptr<Test> wptr1 = wptr;         // Strong reference 1 weak reference 2
}
Copy the code

Giving one Weak_Ptr to another Weak_PTR increases the weak reference count. So, when shared_ptr goes out of scope, its resources are released. What happens to weak_ptr pointing to shared_ptr? Weak_ptr is expired. There are two ways to determine whether weak_ptr points to effective resources:

  • Call use-count() to get the reference count, which returns only a strong reference count, not a weak reference count.
  • Call the expired() method. Faster than calling the use_count() method.

Call Lock () from weak_ptr to get shareD_ptr or directly transform weak_ptr to shared_ptr

Resolve the shared_ptr circular reference problem

class B;
class A
{
public:
 A(  ) : m_a(5) {}; ~A( ) {cout<<" A is destroyed"<<endl;
 }
 void PrintSpB( );
 weak_ptr<B> m_sptrB;
 int m_a;
};
class B
{
public:
 B(  ) : m_b(10) {}; ~B( ) {cout<<" B is destroyed"<<endl;
 }
 weak_ptr<A> m_sptrA;
 int m_b;
};

void A::PrintSpB( )
{
 if( !m_sptrB.expired() )
 {  
  cout<< m_sptrB.lock( )->m_b<<endl; }}void main( )
{
 shared_ptr<B> sptrB( new B );
 shared_ptr<A> sptrA( new A );
 sptrB->m_sptrA = sptrA;
 sptrA->m_sptrB = sptrB;
 sptrA->PrintSpB( );
}
Copy the code

STL smart pointer trap/not smart enough

  1. Use make_shared/make_unique instead of new

STD :: shareD_ptr is implemented using the refcount technique, so there is an internal counter (a control block for managing data) and a pointer to the data. Therefore, STD ::shared_ptr p2(new A) requires data memory first and then internal control block, so two memory requests, whereas STD ::make_shared() requires only one memory request, putting data and control block requests together.

  1. Do not use the same built-in pointer to initialize (or reset) multiple smart Pointers

  2. Do not delete the pointer returned by get()

  3. Do not initialize /reset another smart pointer with get()

  4. For resources managed by the smart pointer, it will only delete the memory allocated by new by default, and pass it a remover if it is not allocated by new

  5. Do not leave the this pointer to smart pointer management

    What happens to the following code? It’s the same mistake. The native pointer this is delivered to m_sp and p management at the same time, which causes this pointer to be deleted twice. It is important to note that the delivery to M_sp and P is not managed correctly. It does not mean that multiple shared_Ptr’s cannot hold the same resource at the same time. Resource sharing between shareD_ptr is achieved through shareD_pTR smart pointer copy and assignment, because this can cause the counter to update; If initialized directly with a native pointer, m_sp and P are unaware of each other’s existence, yet both manage the same place. It is equivalent to “inviting two gods in one temple”.

    class Test{
    public:
        void Do(a){  m_sp =  shared_ptr<Test>(this);  }
    private:
        shared_ptr<Test> m_sp;
    };
    int main(a)
    {
        Test* t = new Test;
        shared_ptr<Test> p(t);
        p->Do();
        return 0;
    }
    Copy the code
  6. Do not assign a native pointer to multiple shareD_ptr or unique_pTR management

We know that smart pointer objects treat native Pointers as their own managed resources when initializing smart Pointers using native Pointers. After initializing multiple smart Pointers, they all free memory. This will result in the native pointer being released multiple times!!

```C++ int* ptr = new int; shared_ptr<int> p1(ptr); shared_ptr<int> p2(ptr); // PTR will be freed multiple times when p1 and P2 are destructed. ` ` `Copy the code
  1. Instead of using new to come out of the space to customize the remover

The following code attempts to assign dynamic memory generated by malloc to Shared_Ptr for management; There is clearly a problem with delete and malloc. [](int* p){free(p); } to shared_ptr.

```C++ int main() { int* pi = (int*)malloc(4 * sizeof(int)); shared_ptr<int> sp(pi); return 0; } ` ` `Copy the code
  1. Try not to use get()

The designers of smart Pointers provide the GET () interface so that smart Pointers can also accommodate the related functions used by native Pointers. The design is either a good design or a failure. Because we deliver native Pointers to smart pointer management according to the closed principle of encapsulation, we should not and cannot get native Pointers; Because the only manager of a native pointer should be a smart pointer. Not some other code in the client logic area. So we need to be extra careful when using get(). Do not use the native pointer returned by get() to initialize or release other smart Pointers. (Can only be used, not managed). The following code violates this rule:

int main(a)
{
    shared_ptr<int> sp(new int(4));
    shared_ptr<int> pp(sp.get());
    return 0;
}
Copy the code

Reference

  1. cppreference.com
  2. C++11 smart pointer author: kabbalah tree
  3. C++11 and C++14 standard smart pointer
  4. Item 21: Use STD ::make_unique and STD ::make_shared in preference to using new directly
  5. C++ dynamic memory resource management (5) – smart pointer trap