Common [memory leak] posture

Posted by jasonbullard on Tue, 18 Jan 2022 18:30:18 +0100

Focus on the official account [high-performance architecture exploration], dry cargo for the first time; Reply to [pdf] and get classic computer books for free

This article is excerpted from the article:
Memory leaks - causes, avoidance, and location

This paper summarizes several common ways of memory leakage. Paying attention to these points can avoid more than 95 +% memory leakage

Not released

This is very common, such as the following code:

int fun() {
    char * pBuffer = malloc(sizeof(char));
    
    /* Do some work */
    return 0;
}

The above code is a very common memory leak scenario (you can also use new to allocate). We applied for a piece of memory, but did not call the free function to free the memory at the end of the fun function.

In C + + development, there is also a memory leak, as follows:

class Obj {
 public:
   Obj(int size) {
     buffer_ = new char;
   }
   ~Obj(){}
  private:
   char *buffer_;
};

int fun() {
  Object obj;
  // do sth
  return 0;
}

In the above code, the destructor does not release the member variable buffer_ When writing the destructor, we must carefully analyze whether the member variable applies for dynamic memory. If so, it needs to be released manually. We have rewritten the destructor, as follows:

~Object() {
  delete buffer_;
}

In C/C + +, for ordinary functions, if heap resources are applied, please follow up the specific scenario of the code and call free / delete to release resources; For class, if you have applied for heap resources, you need to call free/delete in the corresponding destructor for resource release.

Unmatched

In C + +, we often use the new operator to allocate memory, which mainly does two things:

  1. Apply for memory from the heap through operator new (under glibc, malloc is called at the bottom of operator new)
  2. Call the constructor (if the operand is a class)

Correspondingly, the delete operator is used to free memory in the reverse order of new:

  1. Call the destructor of the object (if the operation object is a class)
  2. Free memory through operator delete
void* operator new(std::size_t size) {
    void* p = malloc(size);
    if (p == nullptr) {
        throw("new failed to allocate %zu bytes", size);
    }
    return p;
}
void* operator new[](std::size_t size) {
    void* p = malloc(size);
    if (p == nullptr) {
        throw("new[] failed to allocate %zu bytes", size);
    }
    return p;
}

void  operator delete(void* ptr) throw() {
    free(ptr);
}
void  operator delete[](void* ptr) throw() {
    free(ptr);
}

In order to deepen the understanding of many aspects, let's take an example:

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

int main() {
  Test *t = new Test;
  // do sth
  delete t;
  return 0;
}

In the above main function, we use the new operator to create a Test class pointer

  1. Apply for memory through operator new (the underlying malloc Implementation)
  2. Call the constructor on the memory block requested above through placement new
  3. Call PTR - > ~ Test() to release the member variables of the Test object
  4. Call operator delete to free memory

The above process can be understood as follows:

// new
void *ptr = malloc(sizeof(Test));
t = new(ptr)Test
  
// delete
ptr->~Test();
free(ptr);

Well, in the above content, we briefly explained the basic implementation and logic of new and delete operators in C + +. Then, we will briefly summarize several types of memory leakage.

new and free

Still take the above Test object as an example, and the code is as follows:

Test *t = new Test;
free(t)

Memory leakage will occur here. As we have analyzed above, the new operator will first allocate a block of memory through operator new, and then call placement new in the block of memory, that is, call the constructor of Test. In the above code, the memory is only released through the free function, but the destructor of Test is not called to release the member variables of Test, resulting in memory leakage.

new [] and delete

int main() {
  Test *t = new Test [10];
  // do sth
  delete t;
  return 0;
}

In the above code, we create an array of Test type through new, then delete the array through the delete operator, compile and execute, and the output is as follows:

in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in ~Test

As can be seen from the above output results, the constructor was called 10 times, but the destructor was called only once, resulting in memory leakage. This is because calling delete t frees the memory requested through operator new [], that is, the memory block requested by malloc, and only calls the destructor of t[0] object, but the destructor of t[1... 9] object is not called.

Virtual deconstruction

I remember when I met Google in 2008, the interviewer asked, can std::string be inherited and why?

I didn't answer at that time. Later, shortly after the interview, I happened to see that inheritance requires the parent destructor to be virtual. I suddenly realized that the original investigation point was here.

Let's look at the destructor definition of std::string:

~basic_string() { 
  _M_rep()->_M_dispose(this->get_allocator()); 
}

This needs special instructions. std::basic_string is a template, and std::string is a specialization of the template, that is, std::basic_string

typedef std::basic_string<char> string;

Now we can give the answer to this question: No, because the destructor of std::string is not virtual, which will cause memory leakage.

It is still proved by an example.

class Base {
 public:
  Base(){
    buffer_ = new char[10];
  }

  ~Base() {
    std::cout << "in Base::~Base" << std::endl;
    delete []buffer_;
  }
private:
  char *buffer_;

};

class Derived : public Base {
 public:
  Derived(){}

  ~Derived() {
    std::cout << "int Derived::~Derived" << std::endl;
  }
};

int main() {
  Base *base = new Derived;
  delete base;
  return 0;
}

The above code output is as follows:

in Base::~Base

It can be seen that the above code does not call the destructor of the Derived class. If the Derived class applies for resources on the heap, a memory leak will occur.

To avoid memory leakage due to inheritance, we need to declare the destructor of the parent class as virtual. The code is as follows (only some modified codes are listed, others remain unchanged):

~Base() {
    std::cout << "in Base::~Base" << std::endl;
    delete []buffer_;
  }

Then re execute the code, and the output results are as follows:

int Derived::~Derived
in Base::~Base

With the help of this paper, we summarize the call order of constructor and destructor in the case of inheritance.

Constructor call order of derived class objects when they are created:

  1. Call the constructor of the parent class
  2. Call the constructor of the parent class member variable
  3. Call the constructor of the derived class itself

Destructor calling order of derived class object during destructor:

  1. Executes the destructor of the derived class itself
  2. Executes the destructor of a derived class member variable
  3. Executes the destructor of the parent class

In order to avoid memory leakage when there is an inheritance relationship, please follow a rule: whether the derived class applies for resources on the heap or not, please declare the destructor of the parent class as virtual.

Circular reference

In C + + development, in order to avoid memory leakage as much as possible, smart pointer has been introduced since C++11. The common one is shared_ptr,weak_ptr and unique_ptr, etc. (auto_ptr has been abandoned), where weak_ptr exists to solve circular reference, which is often different from shared_ptr.

Next, let's look at a code:

class Controller {
 public:
  Controller() = default;

  ~Controller() {
    std::cout << "in ~Controller" << std::endl;
  }

  class SubController {
   public:
    SubController() = default;

    ~SubController() {
      std::cout << "in ~SubController" << std::endl;
    }

    std::shared_ptr<Controller> controller_;
  };

  std::shared_ptr<SubController> sub_controller_;
};

int main() {
  auto controller = std::make_shared<Controller>();
  auto sub_controller = std::make_shared<Controller::SubController>();

  controller->sub_controller_ = sub_controller;
  sub_controller->controller_ = controller;
  return 0;
}

Compile and execute the above code and find that the destructors of Controller and SubController are not called. We try to print the reference count. The code is as follows:

int main() {
  auto controller = std::make_shared<Controller>();
  auto sub_controller = std::make_shared<Controller::SubController>();

  controller->sub_controller_ = sub_controller;
  sub_controller->controller_ = controller;

  std::cout << "controller use_count: " << controller.use_count() << std::endl;
  std::cout << "sub_controller use_count: " << sub_controller.use_count() << std::endl;
  return 0;
}

After compilation and execution, the output is as follows:

controller use_count: 2
sub_controller use_count: 2

From the above output, it can be found that since the reference count is 2, controller and sub will not be called at the end of the main function_ Controller's destructor, so there is a memory leak.

The reason for the above memory leak is what we often call circular reference.

To solve STD:: shared_ For memory leakage caused by PTR circular reference, we can use std::weak_ptr to remove the cycle in the figure above.

class Controller {
 public:
  Controller() = default;

  ~Controller() {
    std::cout << "in ~Controller" << std::endl;
  }

  class SubController {
   public:
    SubController() = default;

    ~SubController() {
      std::cout << "in ~SubController" << std::endl;
    }

    std::weak_ptr<Controller> controller_;
  };

  std::shared_ptr<SubController> sub_controller_;
};

In the above code, we will the controller in the SubController class_ The type of is from std::shared_ptr becomes std::weak_ptr, recompile and execute, and the results are as follows:

controller use_count: 1
sub_controller use_count: 2
in ~Controller
in ~SubController

As can be seen from the above results, controller and sub_ The controller is released, so the memory leak caused by circular reference can also be solved.

One might ask, use std::shared_ptr can directly access the corresponding member function. If it is std::weak_ptr, how to access it? We can use the following methods:

std::shared_ptr controller = controller_.lock();

That is, in the subclass SubController, if you want to use the controller to call its corresponding function, you can use the above method.

Focus on the official account [high-performance architecture exploration], dry cargo for the first time; Reply to [pdf] and get classic computer books for free

Topics: C++ data structure Memory Leak