C++ memory management

We have already been exposed to memory management in the study of C language, so what are the differences and similarities between C++ and C language?

C++ memory distribution

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";
	const char* pChar3 = "abcd";
	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. Multiple choice questions:
options : A.the stack B.heap C.Data segment (static area) D.Code segment (constant area)
    
globalVar Where?_data segment___ staticGlobalVar Where?__data segment__
staticVar Where?__data segment__ localVar Where?__the stack__
num1 Where?__the stack__
char2 Where?__the stack__ *char2 Where?_the stack__
pChar3 Where?__the stack__ *pChar3 Where?__code snippet__
ptr1 Where?__the stack__ *ptr1 Where?__heap__
2. Fill in the blanks:
sizeof(num1) = __40__;
sizeof(char2) = __5__; strlen(char2) = __4__;
sizeof(pChar3) = __4/8__; strlen(pChar3) = __4__;
sizeof(ptr1) = __4/8__;

We can complete the above content by learning C language, and we can better understand its characteristics through the storage location of different variables. The memory partition is as follows:

[illustrate]:

  1. The stack is also called the stack, which stores non-static local variables, function parameters, return values, etc., and the stack grows downward.
  2. Memory-mapped segments are efficient I/O mappings for loading a shared dynamic memory bank. Users can use the system interface to create shared shared memory for inter-process communication. (If you haven't learned this in the Linux course, you only need to understand it now).
  3. The heap is used for dynamic memory allocation when the program is running, and the heap can grow upwards.
  4. Data segment - stores global data and static data.
  5. Code Segment -- Executable code/read-only constants.

C language dynamic memory management

C language provides memory management functions such as malloc, calloc, realloc and free

void mem_alloc ()
{
	int* p1 = (int*) malloc(sizeof(int));
	free(p1);
	// 1. What is the difference between malloc/calloc/realloc?
	int* p2 = (int*)calloc(4, sizeof (int));
	int* p3 = (int*)realloc(p2, sizeof(int)*10);
	// Is free(p2) needed here?
	free(p3);
}

We have already learned the differences and characteristics of the above three functions, so I won’t go into details.

There is no need for free(p2) here, realloc operates on p2, and the space pointed to by p2 can be expanded in two ways:

  1. In-situ expansion;
  2. Off-site expansion;

Either way, the expanded space can be handed over to p3 for full management, and there is no need to operate p2, so free(p2);

C++ memory management method

C++ is compatible with C language, so the C language memory management method can still be used in C++, but some places are powerless and it will be troublesome to use, so C++ has proposed its own memory management method: through new and delete operators Dynamic memory management.

So how to use them for memory management?

new and delete operate on built-in types

void mem_alloc()
{
	// Dynamically apply for a space of type int
	int* ptr1 = new int;
	// Dynamically apply for an int type space and initialize it to 10
	int* ptr2 = new int(10);
	// Dynamically apply for 3 spaces of type int
	int* ptr3 = new int[3];
	// Dynamically apply for 3 spaces of type int, and initialize them to 2, 3, 0 respectively
	int* ptr4 = new int[3]{ 2,3 };//Insufficient default initialization to 0
	delete ptr1;
	delete ptr2;
	delete[] ptr3;
	delete[] ptr4;
}

When using new to apply for memory, new only needs to declare the type and number, and can initialize the requested space.

  1. Parentheses () can be used for initialization when only applying for a built-in type variable;
  2. When applying for multiple built-in type variables, use square brackets [] to declare the number of variables, and use {} to initialize multiple variables, which is the same as the use of {} when initializing an array;

It can be seen that when applying for a built-in type space, there is no difference between new and malloc, delete and free. Except for explicit initialization, compared with traditional malloc, new does not automatically initialize variables and other special operations. Processing, and the role of delete seems to be the same as free.

Note: To apply and release space for a single element, use new and delete operators, to apply and release continuous space, use new[] and delete[].

new and delete operations on custom types

class Test
{
public:
	Test()
		: _data(0)
	{
		cout << "Test():" << this << endl;
	}
	~Test()
	{
		cout << "~Test():" << this << endl;
	}
private:
	int _data;
};
void Test1()
{
	// Apply for a single space of type Test
	Test* p1 = (Test*)malloc(sizeof(Test));
	free(p1);
	// Apply for 3 spaces of Test type
	Test* p2 = (Test*)malloc(sizeof(Test) * 3);
	free(p2);
}
void Test2()
{
	// Apply for a single object of type Test
	Test* p1 = new Test;
	delete p1;
	// Apply for 3 objects of type Test
	Test* p2 = new Test[3];
	delete[] p2;
}
int main()
{
	cout << "Test1:" << endl;
	Test1();
	cout << endl << endl;
	cout << "---------------------------------------------------------------------" << endl;
	cout << "Test2:" << endl;
	Test2();
	return 0;
}

output:

Test1:

Test2:
Test():00000229F58AD590
~Test():00000229F58AD590
Test():00000229F58B0848
Test():00000229F58B084C
Test():00000229F58B0850
~Test():00000229F58B0850
~Test():00000229F58B084C
~Test():00000229F58B0848

Through the above code, we can find the difference between new and malloc. When applying for a custom type of space, new will call the constructor, and delete will call the destructor, but malloc and free do not.

new operation initialization of custom types

We already know that new will call the constructor of the custom type, so how to explicitly call the constructor initialization for the space application of the custom type?

class T
{
public:
	T(int m = 0, int n = 0)
		:
		_m(m),
		_n(n)
	{
		cout << "T(int m = 0, int n = 0):" << this << endl;
	}
	~T()
	{
		cout << "~T():" << this << endl;
	}
private:
	int _m;
	int _n;
};

int main()
{
	T* p1 = new T[3]{ {1,2},{4,5} };//Initialization method when applying for multiple custom objects
	delete[] p1;
	cout << endl << endl;
	T* p2 = new T(8, 9);//The initialization method when applying for a custom object
	delete p2;
	return 0;
}

output:

T(int m = 0, int n = 0):000001B53A5807E8
T(int m = 0, int n = 0):000001B53A5807F0
T(int m = 0, int n = 0):000001B53A5807F8
~T():000001B53A5807F8
~T():000001B53A5807F0
~T():000001B53A5807E8

T(int m = 0, int n = 0):000001B53A5827F0
~T():000001B53A5827F0

  1. For the initialization of applying for a custom type space, use () or {} to enclose a value or a set of values ​​​​to be initialized;
  2. For the initialization of applying for multiple custom type spaces, you can use {} brackets to initialize multiple values ​​or groups of values ​​to be initialized;

operator new and operator delete

operator new and operator delete are not overloads of new and delete. new and delete are operators for users to apply and release dynamic memory. operator new and operator delete are global functions provided by the system. new calls the global function of operator new at the bottom layer to apply Space, delete uses the operator delete global function at the bottom to release space.

/*
operator new: This function actually applies for space through malloc. When malloc successfully applies for space, it returns directly; if space application fails, try to implement countermeasures for insufficient space. If the countermeasures are set by the user, continue to apply, otherwise an exception is thrown.
*/
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, a bad_alloc type exception will be thrown here
			static const std::bad_alloc nomem;
			_RAISE(nomem);
		}
	return (p);
}
/*
operator delete: This function finally releases 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 realization of
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

Through the implementation of the above two global functions, we know that operator new actually applies for space through malloc. If the malloc application space is successful, it returns directly. Otherwise, it executes the countermeasures when the space provided by the user is insufficient. If the user provides this measure, it continues to apply. Otherwise an exception is thrown. operator delete finally releases space through free.

The process of new is as follows:

The operator new process is as follows:

The delete process is as follows:

The operator delete process is as follows:

_free_dbg and free are actually the same, free is not a function but a macro, free is actually _free_dbg,

  1. What new does:

​ new actually calls the operator new function first, and operator new encapsulates the malloc function. If malloc fails, an exception will be thrown. If malloc succeeds, it will return. After the memory application, the constructor is called to initialize the object on the allocated memory (custom type), and then returns a pointer to the constructed object.

  1. What delete does:

​ delete first calls the destructor (custom type) pointing to the object, and then calls the operator delete function, and the operator delete encapsulates _free_dbg to release the memory.

Class-specific overloading of operator new and operator delete

The following code demonstrates that for the node ListNode of the linked list, by overloading the class-specific operator new/operator delete, the linked list node uses the memory pool to apply for and release memory, improving efficiency.

memory pool technology

Memory Pool (Memory Pool) is a memory allocation method. Usually, we are used to directly using new, malloc and other API s to apply for memory allocation. The disadvantage of this is: due to the variable size of the requested memory block, frequent use will cause a large number of memory fragments and further reduce performance.

The memory pool technology can improve the efficiency of dynamically applying for memory to a certain extent in some specific cases, and has the following advantages:

  1. Reduce the number of new and delete, and reduce the running time;
  2. Avoid memory fragmentation;

In terms of efficiency:

​ C language uses malloc/free to allocate memory, and C++ uses new/delete to allocate memory. Their memory application and release all interact with the operating system. The specific content is described in Chapter 8 of Yan Weimin's data structure. The main thing is that the system needs to maintain a memory linked list. When a memory is requested, it will find a suitable memory in the linked list and allocate it to it according to the corresponding allocation algorithm. Some of these algorithms allocate the first found memory block that is not smaller than the requested memory, some allocate the largest memory block, and some allocate the memory block closest to the requested memory size. The allocated memory block may be larger than the requested memory size, so it needs to be cut and the remaining memory will be inserted into the free list. When freeing, the system may organize the memory to determine whether there is free space before and after the free memory block, and if so, merge it. In addition, new/delete also considers multi-threaded applications, and needs to be locked every time memory is allocated and released, which also increases overhead. In a word, calling the memory allocation function in the library is very time-consuming.

Memory Fragmentation:

​ What is memory fragmentation, it is easy to understand from the literal meaning, that is, the memory is no longer a whole piece, but broken. Because of this continuous new/delete operation, a large block of memory can be divided into small memory and allocated, and these small memories are not continuous. When you allocate large contiguous memory, although the sum of the remaining memory may be greater than the size of the memory to be allocated, the system cannot find contiguous memory, which leads to allocation errors. malloc will cause NULL to be returned, while new will throw an exception.

​ Does it mean that the default memory management function is not good? Of course not. The default memory allocation and release algorithms naturally also consider performance, but the general versions of these memory management algorithms need to do more extra work in order to cope with more complex and wider situations. For a specific application, a custom memory pool suitable for its own specific memory allocation and release mode can achieve better performance.

​ It can be seen that if the application frequently allocates and releases memory on the heap, it will result in a loss of performance. And it will cause a lot of memory fragmentation in the system, reducing the utilization rate of memory.

How to solve

To solve the above two problems, the best way is the memory pool technology. The specific method is to fix the size, apply in advance, and reuse.

Because the application and release of memory is very inefficient, we only apply for a large piece of memory at the beginning (when the piece of memory is not enough, we allocate it twice), and then take it out of this piece of memory every time we need it, and Mark this memory as used, and mark this memory as freed when released. When releasing, the memory is not really released to the operating system, only when a large block of memory is free, it is released to the operating system. In this way, the number of new/delete operations is reduced, thereby improving efficiency.

When calling the memory allocation function, most of the time the allocated memory size is fixed, so you can allocate a fixed size memory block each time, thus avoiding the possibility of memory fragmentation.

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _data;
	void* operator new(size_t n)
	{
		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;
	}
};
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;
	}
private:
	ListNode* _head;
};
int main()
{
	List l;
	return 0;
}

Implementation principle of new and delete

built-in type

If you apply for a built-in type of space, new and malloc, delete and free are basically similar, the difference is: new/delete applies for and releases the space of a single element, new[] and delete[] apply for continuous space, Moreover, new will throw an exception when it fails to apply for space, and malloc will return NULL.

custom type

  • The principle of new
    1. Call the operator new function to apply for space;
    2. Execute the constructor on the requested space to complete the construction of the object;
    3. Return the address of the constructed object;
  • The principle of delete
    1. Execute the destructor on the space to complete the resource cleanup work in the object;
    2. Call the operator delete function to release the space of the object;
  • The principle of new T[N]
    1. Call the operator new[] function, and actually call operator new in operator new[] to complete the application for N objects;
    2. Execute the constructor N times on the requested space;
  • The principle of delete[]
    1. Execute N times of destructors on the object space to be released to complete the cleanup of resources in N objects;
    2. Call operator delete[] to release space, and actually call operator delete in operator delete[] to release object space;

Position new expression (placement-new)

Positioning the new expression refers to calling the constructor to initialize an object in the allocated original memory space.

Use the format:

new (place_address) type or new (place_address) type(initializer-list)

place_address must be a pointer, and initializer-list is the initialization argument list of the type

scenes to be used:

Positioning new expressions are generally used in conjunction with memory pools in practice. Because the memory allocated by the memory pool is not initialized, if it is an object of a custom type, you need to use the definition expression of new to explicitly call the constructor for initialization.

class Test
{
public:
	Test()
		: _data(0)
	{
		cout << "Test():" << this << endl;
	}
	~Test()
	{
		cout << "~Test():" << this << endl;
	}
private:
	int _data;
};
int main()
{
	// What pt now points to is just a piece of original space with the same size as the Test object, and it cannot be regarded as an object, because the constructor is not executed
	Test* p = (Test*)malloc(sizeof(Test));
	new(p) Test; // Note: If the constructor of the Test class has parameters, parameters need to be passed here
	return 0;
}

common interview questions

The difference between malloc/free and new/delete

What malloc/free and new/delete have in common is that they all apply for space from the heap and need to be released manually by the user. The differences are:

  1. malloc and free are functions, new and delete are operators;
  2. The space requested by malloc will not be initialized, and new can be explicitly initialized;
  3. When malloc applies for space, it is necessary to manually calculate the size of the space and transfer it, and new only needs to follow it with the type of space;
  4. The return value of malloc is void*, which must be forced when used, and new does not need it, because new is followed by the type of space;
  5. When malloc fails to apply for space, it returns NULL, so it must be judged as empty when using it, new does not need it, but new needs to catch exceptions;
  6. When applying for a custom type of object, malloc/free will only open up space, and will not call the constructor and destructor, while new will call the constructor to complete the initialization of the object after applying for space, and delete will call the destructor before releasing the space Complete the cleanup of resources in the space;

memory leak

What is a memory leak

What is a memory leak: A memory leak refers to a situation in which a program fails to release memory that is no longer in use due to negligence or error. A memory leak does not refer to the physical disappearance of memory, but rather the loss of control over the memory segment due to a design error after the application allocates a certain segment of memory, resulting in a waste of memory.

The dangers of memory leaks

Hazards of memory leaks: Memory leaks occur in long-running programs, which have a great impact, such as operating systems, background services, etc. Memory leaks will lead to slower and slower responses, and eventually freeze.

double division(int a, int b)
{
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (a / b);
}

int main()
{
	int x = 50;
	int y = 0;
	double z = 0;
	// 1. The memory application forgets to release
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;
	// 2. Abnormal security issues
	int* p3 = new int[10];
	division(1, 0); // Here, the function throws an exception and delete[] p3 is not executed, and p3 is not released.
	delete[] p3;
	return 0;
}

Classification of memory leaks

In C/C++ programs, we generally care about two aspects of memory leaks:

  • heap memory leak

    Heap memory refers to a piece of memory allocated from the heap through malloc / calloc / realloc / new as needed during program execution. After use, it must be deleted by calling the corresponding free or delete. Assuming that the design error of the program causes this part of the memory to not be released, then this part of the space will no longer be used in the future, and Heap Leak will occur.

  • system resource leak

    Refers to the resources allocated by the system used by the program, such as sockets, file descriptors, pipes, etc., which are not released using the corresponding functions, resulting in waste of system resources, which can seriously lead to reduced system performance and unstable system execution.

How to detect memory leaks

How to avoid memory leaks

  1. Good design specifications in the early stage of the project, develop good coding standards, and remember to release the memory space that matches. ps: This ideal state. But if you encounter an exception, even if you pay attention to release, there may still be problems. It needs to be managed by the next smart pointer to be guaranteed.
  2. Use RAII ideas or smart pointers to manage resources.
  3. Some internal company specifications use internally implemented private memory management libraries. This library comes with options for memory leak detection.
  4. If something goes wrong, use memory leak tools to detect, but many tools are not reliable enough, or the fees are expensive.

How to apply for 4G memory on the heap at one time

/ compile the program into x64 process, run the following program to try?
#include <iostream>
using namespace std;
int main()
{
	void* p = new char[0xfffffffful];
	cout << "new:" << p << endl;
	return 0;
}

Tags: C++

Posted by omanush on Mon, 05 Dec 2022 00:21:38 +1030