C + + memory management


  compare some differences between C and C + + definition categories to Stack as an example:

   C language defines the stack is nothing more than defining a structure and then defining some functions. Its data and functions are separated. When operating data, you need to call the initialization function after defining the type, and then you need to pass the address of the stack object every time you operate the stack data, otherwise you can't change the original stack data in the function. At the end of our use, we need to call the destroy function manually.

   and when calling the stack structure, the data in my structure is not protected. In case the implementation details of everyone's stack are different, for example, some people's top element marks the subscript of the last element at the top of the stack, and some people's top element marks the subscript of the element at the top of the stack. Blind access to data is likely to be wrong due to different implementation details.

   therefore, C + + classes provide the idea of encapsulation, and the data you do not want to call will not be accessed by you.

   but in order to realize the basic functions of the stack, I added member functions in the class. These interfaces are open to you. You can call the interface to access me.

   but there are still problems to be solved. I often forget to call Init function and destroy function, which is also a hidden danger.

   therefore, C + + provides constructors and destructors, which will be called automatically when defining and leaving their own defined cycles.

    but calling each member function always needs to pass the address of the object. It's troublesome every time. This leads to the this pointer of the member function.

   this explains how the syntax of C + + we explained earlier came from.

   considering the problem of deep and shallow copy, users are allowed to provide copy constructor and operator =.

1, Memory distribution of C/C + +

   after the C/C + + program runs, the process address space is distributed as follows:

   the above is the virtual memory distribution of the process when the C program is running. The C + + program inherits this distribution of the C program.

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
 static int staticVar = 1;
 int localVar = 1;
 
 int num1[10] = {1, 2, 3, 4};
 char char2[] = "abcd";
 //Here is a five byte array. Copy a b c d  a B C D \ 0  to the stack
 //So char2 *char2 are on the stack
 const char* pChar3 = "abcd";
 //The read-only constant string "abcd" pointing to the stored in the constant area
 int* ptr1 = (int*)malloc(sizeof (int)*4);
 int* ptr2 = (int*)calloc(4, sizeof(int));
 int* ptr3 = (int*)realloc(ptr2, sizeof(int)*4);
 free (ptr1);
 free (ptr3);
}
1. choice question:
 option: A.Stack B.heap C.Data segment D.Code snippet
 globalVar Where?__C__ staticGlobalVar Where?__C__
 staticVar Where?__C__ localVar Where?__A__
 num1 Where?__A__
 
 char2 Where?__A__ *char2 Where?__A_
 pChar3 Where?__A__ *pChar3 Where?__D__
 ptr1 Where?__A__ *ptr1 Where?__B__

  C language heap memory allocation method: malloc, calloc, realloc

2, Memory allocation in C + +

1. New / delete handles built-in types

  why should C + + provide a new heap memory allocation method?

  first of all, the allocation method of C language is not easy to use.

//Single int
int* p = (int*)malloc(sizeof(int));
//Array of 5 int s
int* p1 = (int*)malloc(5*sizeof(int));
//C + + provides an operator called new
int* p2 = new int;//single
int* p3 = new int[5];//An array of five elements
free(p);
free(p1);
delete p2;//Single operating system
delete[] p3;//Array operating system

  after testing, malloc/free and new/delete have no essential difference on built-in types, but only usage.

  one note:

int* p = new int[5];//An array of five elements
int* p2 = new int(5);//An integer is initialized to 5
//C++98 does not support initializing arrays when new comes
//C++11 supports initialization with braces, as follows:

int main()
{
	int* p1 = new int;
	int* p2 = new int[5];
	int* p3 = new int(5);
	int* p4 = new int[5]{ 1,2,3,4,5 };
}

2. New / delete handle user-defined types

   the real use of introducing new/delete lies in the user-defined type. We hope to call the default constructor for initialization without adding parameters after the user-defined type is defined, but malloc can't do this

  for custom types, malloc calloc realloc can dynamically allocate memory, but they only open space and will not call the constructor of this class.

   while C + + new allocates memory, it also calls constructor initialization.

   free will only return the memory, and will not call the destructor to clean up the heap space that may be opened up by the constructor when creating the object. delete will call the destructor of the custom type first, and then return the memory of the data members on the heap in the object to the heap.

  the test is as follows:

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

int main()
{
	A* a1 = (A*)malloc(sizeof(A));
	A* a2 = (A*)malloc(sizeof(A) * 5);
	A* a3 = new A;
	A* a4 = new A[5];
	free(a1);
	free(a2);
	delete a3;
	delete[] a4;
}

   by observing the following figure, it can be found that the constructor and destructor were called only six times. Breakpoint debugging found that malloc will not call the constructor of custom type to initialize, and free will not call the destructor of custom type to reclaim the heap memory applied by some members of the object in the constructor.

   the mismatch between malloc and free,new and delete does not necessarily cause problems. The specific situation depends on the processing of the compiler, but we'd better match it as far as possible.

   if you don't want to call the default constructor or there is no default constructor, you can write this:

A* a = new A(4);
A* a = new A[5]{1, 2, 3, 4, 5};

  delete[] p; The square brackets are used to know how many objects are in the space pointed to by the pointer P, so as to know how many times to call the destructor to clean up the memory.

  new A[5]; And delete [], new a; To match with delete, delete a is used for an object; The object array uses delete[] a;, So a = new a; An object, and then delete[] a; It may be detected and the program will crash.

  there is another reason for introducing new/delete: for process oriented objects, the method of handling errors is error code; For object-oriented language, let it throw exceptions when there is an error. Naturally, C + + also hopes to use exceptions to deal with the error situation. It also hopes to throw exceptions for memory allocation failure.

   in C language, the application for memory from the heap may fail. For example, after the malloc application fails, a null pointer will be returned, and we can get an error code:

int main()
{
    int* p = (int*)malloc(1024 * 1024 * 1024);
    if (p == nullptr)
    {
        printf("%d\n", errno);
        //Error global variable marking error
        perror("malloc fail");
        //perror can print out symbols
        exit(-1);
    }
}

   however, C + + does not want to identify the allocation failure by returning nullptr and error code. The following will verify that even if the application fails, it will not return null:

int main()
{
	//int* p = (int*)malloc(1024u * 1024u * 1024u* 2u);
	char* p = new char[1024u * 1024u * 1024u * 2u - 1];
	if (p == nullptr)
	{
		printf("%d\n", errno);
		perror("malloc fail");
		exit(-1);
	}
}

   directly report an error instead of returning a null pointer like C, and then enter if to print the error code and handle the error. The error reported here is actually the error that we did not catch the exception.

   write as abnormal style:

int main()
{
	//int* p = (int*)malloc(1024u * 1024u * 1024u* 2u);
	char* p = nullptr;
    try
    {
        //try means that it will detect whether the steps in it will generate exception objects
        //Try to catch the exception and catch the call to catch
        //Here, the header can also be put into the function, because the object is thrown to the global
        //The exception thrown in the function will also be caught.
        p = new char[1024u * 1024u * 1024u * 2u - 1];
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;
    }
}

// It can also be written here

void f()
{
    char* p = new char[1024u * 1024u * 1024u * 2u - 1];
    cout << "f()" << endl;
}

int main()
{
    try
    {
        f();
    }
    // Here, after the application fails in F and an exception is thrown, it will be directly transferred to catch and will not continue to execute cou < < f() "< < endl;
    catch (const exception& e)
    {
        cout << e.what() << endl;
    }
}

   if new fails, it will throw exceptions out. Don't worry about all possible mistakes. It will throw exceptions out and then go catch. It is in line with the idea of object-oriented design.

   in conclusion, C + + provides new/delete, which mainly solves two problems:

  1. new/delete will call constructors and destructors when initializing and cleaning up problems in the active application of custom type objects.

  2. If new fails to apply for memory, exceptions will be thrown out, which is in line with the error handling mechanism of object-oriented language

    ps:delete and free generally do not fail. If they fail, it is because there is cross-border access in the released space or the position of the released pointer is wrong (not the address of the first element of the space).

  for example, use new and delete to complete the application of a linked list node (i.e. buynewnode function)

#include <iostream>
using namespace std;
struct ListNode
{
    int _val;
    ListNode* _prev;
    ListNode* _next;
    ListNode(int val = 0): _val(val), _prev(nullptr), _next(nullptr)
    {
        cout << "ListNode(int val)" << endl;
    }
    ~ListNode()
    {
        cout << "~ListNode" << endl;
    }
}; 
int main()
{
    auto pnode = new ListNode(1);
    cout << pnode->_val << endl;
    delete pnode;
    return 0;                                                                                            
}

  for the stack we simulated, there is a dual intention to return resources.

class Stack                                                                                               {
public:
    Stack(int capacity = 4): _top(0), _capacity(capacity)
    {
        _a = new int[_capacity];
        cout << "Stack(int)" << endl;
    }
    ~Stack()
 	{
    	 delete[] _a;
     	_a = nullptr;
     	_top = 0;
     	_capacity = 0;
     	cout << "~Stack()" << endl;
 	}
private:
    int* _a;
    int _top;
    int _capacity;
};

int main()
{
    Stack* pst = new Stack(10);
    // First stack the space for the stack, then call the stack constructor (to int* _a for the memory on the heap).
    delete pst;
    // First call the destructor of the stack to clean up the resources of the stack (return the memory of int* _a) 
    // Then return the memory of the stack object pointed to by pst to the heap memory space
}

3, The underlying principles of new and delete

1 source code observation

   hit the breakpoint and go to disassembly to see how new and delete run. It is found that:

   it is found that the operator new function is called when new generates instructions. We can also use this function ourselves.

  let's take a look at the source code of operator new and find that it is actually the encapsulation of malloc and an exception thrown when an application fails.

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
    // try to allocate size bytes
 	void *p;
    while ((p = malloc(size)) == 0)
 	if (_callnewh(size) == 0)
 	{
 		// report no memory
 		// If the memory application fails, bad will be thrown here_ Alloc type exception
 		static const std::bad_alloc nomem;
 		_RAISE(nomem);
 	}
 	return (p);
}

   mechanism of throwing exception:

  naturally, there is also operator delete

/*
operator delete: This function finally frees up space through free
*/
void operator delete(void *pUserData)
{
 	_CrtMemBlockHeader * pHead;
 	RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
 	if (pUserData == NULL)
		return;
 	_mlock(_HEAP_LOCK); /* block other threads */
 	__TRY
 	/* get a pointer to memory block header */
 	pHead = pHdr(pUserData);
 	/* verify block type */
 	_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
 	_free_dbg( pUserData, pHead->nBlockUse );
 	__FINALLY
 	_munlock(_HEAP_LOCK); /* release other threads */
 	__END_TRY_FINALLY
 	return;
}
/*
free Implementation of
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

  operator delete is also the encapsulation of free and error throwing exceptions.

   such encapsulation is to make the memory management mode of C + + new/delete more in line with the object-oriented way of handling errors (throwing exceptions).

   the source code of new and delete in VS2019 (go to the definition), but I don't understand it.

void __CRTDECL operator delete(
    void* _Block
    ) noexcept;

void __CRTDECL operator delete(
    void* _Block,
    ::std::nothrow_t const&
    ) noexcept;

void __CRTDECL operator delete[](
    void* _Block
    ) noexcept;

void __CRTDECL operator delete[](
    void* _Block,
    ::std::nothrow_t const&
    ) noexcept;

void __CRTDECL operator delete(
    void*  _Block,
    size_t _Size
    ) noexcept;

void __CRTDECL operator delete[](
    void* _Block,
    size_t _Size
    ) noexcept;

2 summarize the principle

  for built-in types, new and delete are no different from malloc and free, but exceptions will be thrown.

For custom type, new first calls operator new to apply the space of the good object, fails to throw the exception, and then calls the constructor of the object.

Delete first invokes the destructor of the object, then calls operator delete..

   for the array of application objects, operator new [] (in fact, this is also the encapsulation of operator new) will be called first to apply for space for these objects. If the application fails, throw an exception, and then call the constructor for each object.

Delete[] first invokes the destructor of each object, then calls operator delete[] to return the space.

3. Overload the class with its operator new and operator delete

   for example, the linked list is composed of nodes. If each of these nodes goes to the system for application, these memories are discrete small memories, and the speed of continuous application and return will be very slow. We can design a memory pool. When applying for nodes, we can apply to the memory pool, just like you are ready to draw water in order to use water.

  this technology is called pooling technology. It has not only memory pool, but also thread pool and link pool. The reason is similar.

struct ListNode
{
 	ListNode* _next;
 	ListNode* _prev;
 	int _data;
    ListNode(int val = 0): _data(val), _next(nullptr), _prev(nullptr)
    {}
 	void* operator new(size_t n)
 	{
        // Use the space configurator in STL to act as a memory pool
        // In this way, there is no need to find the List application node directly when returning the node, and the system speed will be relatively fast
 		void* p = nullptr;
 		p = allocator<ListNode>().allocate(1);
 		cout << "memory pool allocate" << endl;
 		return p;
 	}	
 	void operator delete(void* p)
   {
 		allocator<ListNode>().deallocate((ListNode*)p, 1);
 		cout << "memory pool deallocate" << endl;
 	}
};

// ListNode* newnode = new ListNode(1);
// Overloaded operator new(sizeof(ListNode)) + ListNode(1)

class List
{
public:
 	List()
 	{
 		_head = new ListNode;
 		_head->_next = _head;
 		_head->_prev = _head;
 	}
 	~List()
 	{
 		ListNode* cur = _head->_next;
 		while (cur != _head)
 		{
 			ListNode* next = cur->_next;
 			delete cur;
     		cur = next;
 		}
 		delete _head;
    	_head = nullptr;
 	}
    void push_back(int val)
    {
        ListNode* newnode = new ListNode(val);
        ListNode* tail = _head->prev;
        tail->next = newnode;
        newnode->prev = tail;
        newnode->next = _head;
        _head->prev = newnode;
    }
private:
 	ListNode* _head;
};
int main()
{
    List l;
    l.push_back(1);
    l.push_back(2);
    l.push_back(3);
    l.push_back(4);
    l.push_back(5);
    l.push_back(6);
    l.push_back(7);
    return 0;
}

  you can see that the operator new here is all the memory pool we overload ourselves:

4 positioning new expression (placement new)

   suppose we have applied for a piece of memory, but we do not use the memory applied by new and do not call the constructor of the class. Can we call the constructor of the class for this memory? The answer is yes. Although we can't call the constructor directly, we can use placement new

// Suppose you already have a class A
int main()
{
    A* p1 = (A*)malloc(sizeof(A));
    // Call constructor shown
    new(p1)A(2);
}

   if you apply for memory from the memory pool, because there are many different types of variables that need to apply for memory, you must reinitialize the memory after applying. How to initialize? Just place new. (new (address) class name (constructor parameter)).

   how to destruct? Call destructors that can be displayed.

A* p2 = new A(2);

delete p2;

//Equivalent to

A* p3 = operator new(sizeof(A));
new(p3)A(2);

p3->~A();
operator delete(p3);

5. Difference between new / delete and malloc/free

   differences in usage, whether the constructor destructor is called or not, one group is the operator and the other group is the library function, throwing exceptions and returning null pointers.

6 memory leak

   after the dynamic memory is applied, it will not be returned to the system after it is not used.

  hazards of memory leakage:

  • If the memory is not returned after applying for memory, that is, there is a memory leak, but the process corresponding to this program ends normally, and the system will recover the memory of the process. At this time, the memory leak will not cause harm;
  • The process with memory leakage ends abnormally, such as zombie process. If the memory is not recycled normally, there will be harm.
  • For long-term running programs, such as server programs and background programs, memory leakage will do great harm, because the memory will not end. If the memory of the program leaks, the memory will gradually decrease, and then the card will change or slow down.

  how to prevent memory leakage? First, smart pointers can be used, and then some detection tools can be used.

  how many gigabytes of heap space can you apply for at most? The general application 2G(0x7fffffff or 1024u * 1024u * 1024u * 2 - 1) under 32 bits is sent,

After   is cut to 64 bits, the process address space will become larger, 2 ^ 64 bytes, which is large enough.

Keywords: C++ Back-end

Added by venradio on Sun, 20 Feb 2022 18:48:37 +0200