[C/C++] Detailed explanation of memory management

Memory layout mind map

insert image description here
The memory space allocated for the operating system kernel is called kernel space , and the space allocated by other programs is user space . Different operating systems occupy different proportions of memory, and we need to understand that user .

C++ is compatible with C language, their memory distribution is the same , apply or release space in user space. It's just that C++ adds new methods because of the existence of the object-oriented feature.

  • The picture above is a mind map of the memory layout, and the mind map of the entire blog is placed in the final summary section

1. C/C++ memory distribution

Let's compare the pictures to observe which part of the memory space different data is stored in (if you are not familiar with C language memory management, you can check this blog: [C Language] Memory Management ):

insert image description here

  • Stack: Also called stack – non-static local variables/function parameters/return values, etc., the stack grows downward.
  • Memory-mapped segment: is efficient I / O映射方式and is used to load a shared dynamic memory bank. Users can use the system interface to create shared shared memory for process communication.
  • Heap: Used for dynamic memory allocation when the program is running, the heap can grow upwards.
  • Data segment: stores global data and static data.
  • Code Segment: executable code/read-only constants

Then we explain the code distribution in the above picture as follows (the code in the above picture is placed after the explanation, if necessary, you can copy and run it yourself)

Data segment:

Global variables, static variables created by static modification must be stored in the data segment , where static data and global data are stored, there is no objection to this.

stack:

The localNum variable and num1 array are normally created in the space opened on the stack, which is also easy to understand.

As for char2, pChar3, ptr1, ptr2, and ptr3 , we need to understand that whether it is an array or a pointer, whether it receives a constant string or the address of a space created by the programmer, they are in the function and on the stack after all. The created variables are only used to store data, and their addresses as variables are on the stack.

code snippet:

In the C language, we should know that the data enclosed in double quotes is a constant string. Such a string is a read-only constant and cannot be modified . It is stored in the code segment (constant area). This is stipulated. We understand Just remember, but there are some confusing places that need to be explained:

  1. The results of receiving constant strings in the form of arrays and pointers are different.

    Receive using an array: As shown in the figure above char char2[]="abcd";, the constant string is stored in the code segment. When assigning a value, char2 creates an array of the same size on the stack, and stores each character in the array by copying (including the final '\0' termination character), so we can modify the data in the array char2.

    insert image description here

    Receive using a pointer:

    • C++: As shown in the figure above const char* pChar3 = "abcd";, this is easy to understand, that is, simply store the first address of the constant string in the pointer variable pChar, so the address of pChar3 is on the stack, and the address stored in the pointer variable pChar3 is in the code segment . Because constant strings cannot be modified, changes in permissions are not allowed in C++, and pointers must be modified with the const keyword.

      insert image description here

    • C: In C language, if you use a pointer to receive a string constant, you don’t need to use const to modify the pointer, and even we can use a pointer to modify the constant string. The compiler will not report an error, but such a program cannot run. The line that the constant string is modified will stop (as shown in the figure below), so it is best to use const modification when we encounter this situation in C language.

      insert image description here

heap:

The space created by the programmer using malloc, calloc, and realloc must be placed on the heap. For such a space, the programmer needs to use free to release it by himself. After the space is opened on the heap, pointers are used to receive the first addresses of these spaces, so the pointer variables that receive these addresses are opened on the stack, but the addresses they store are on the heap.

code show as below:

int globalNum = 1;
static int staticGlobalNum = 1;

int main()
{
    
    
	static int staticNum = 1;
	int localNum = 1;
	int num1[10] = {
    
     1,2,3,4 };
	char char2[] = "abcd";
	const char* pChar3 = "abcd";
	int* ptr1 = (int*)malloc(sizeof(int));
	int* ptr2 = (int*)calloc(4, sizeof(int));
	int* ptr3 = (int*)realloc(ptr2,sizeof(int) * 4);

	free(ptr1);
	free(ptr3);

	return 0;
}

2. Dynamic memory management in C language

The ways of dynamically opening up memory in C language are malloc, calloc, and realloc, and finally release the opened space by free. This is an object that must be mastered when learning C, and related questions are often asked during interviews. We pass the following two Let's take a look at the interview questions

1. What is the difference between malloc/calloc/realloc?

  • malloc applies for space to the heap area, and the parameter is the size of the requested memory
  • calloc applies for space from the heap area, the first parameter is the initial value of each byte of the application space, and the second parameter is the size of the application space
  • realloc applies for space from the heap area. The first parameter is the address to be adjusted, and the second parameter is the size of the new space after adjustment. If the first parameter is empty, realloc and malloc are the same.
  • The space applied to the heap must be freed to prevent memory leaks.

2. The realization principle of malloc

(It is more complicated, it is recommended to learn together with the operating system)

3. C++ memory management method

The methods of managing memory in C language (malloc, calloc, realloc, free) can continue to be used in C++, but there are some places where they can’t do anything, and it’s more troublesome to use, such as custom types, so C++ proposes its own memory management method: Memory management through new and delete operators.

3.1 new/delete operation built-in type

We use inttypes as classes

  1. Dynamically apply for a space of type int

    int* ptr1 = new int;//申请空间
    delete ptr1;//释放空间
    

    Direct new followed by the type can apply for space to the heap.

  2. Dynamically apply for an int type space and initialize it to 10

    int* ptr2 = new int(10);//申请空间
    delete ptr2;//释放空间
    

    Add parentheses after the type, and fill in the value that needs to be initialized

    • If no value is added in parentheses, the default initialization is 0
  3. Dynamically apply for 10 spaces of type int

    int* ptr3 = new int[10];//申请空间
    delete[] ptr3;//释放空间
    

    Add square brackets after the type, and fill in the number of spaces of the same type that need to be applied for

  4. Dynamically apply for 10 consecutive int spaces and initialize them

    Add parentheses after the square brackets, do not add any value inside them , and initialize to 0 by default

    int* ptr4_1 = new int[10]();//申请空间
    delete[] ptr4_1;//释放空间
    

    Add brackets after the square brackets, fill in the value of the array, and the insufficient part defaults to 0

    int* ptr4_2 = new int[10] {
          
          1,2,3,4};//申请空间
    delete[] ptr4_2;//释放空间
    

Note: To apply for and release the space of a single element, use the new and delete operators to apply for and release continuous space, use new[] and delete[], they need to be matched and used, and cannot be used indiscriminately. (See 6. Operators that must match for the reason )

Use the following code to check whether the creation of the space is successful

int main()
{
    
    
	int* ptr1 = new int;
	int* ptr2_1 = new int(10);
	int* ptr2_2 = new int();
	int* ptr3 = new int[4];
	int* ptr4_1 = new int[4]();
	int* ptr4_2 = new int[4]{
    
     1,2 };

	printf("ptr1:%p %d\n", ptr1, *ptr1);
	printf("ptr2_1:%p %d\n", ptr2_1,*ptr2_1);
	printf("ptr2_2:%p %d\n", ptr2_2, *ptr2_2);
	for (int i = 0; i < 4; i++)
	{
    
    
		printf("ptr3:%p %d\n", &ptr3[i], ptr3[i]);
	}
	for (int i = 0; i < 4; i++)
	{
    
    
		printf("ptr4_1:%p %d\n", &ptr4_1[i], ptr4_1[i]);
	}
	for (int i = 0; i < 4; i++)
	{
    
    
		printf("ptr4_2:%p %d\n", &ptr4_2[i], ptr4_2[i]);
	}

	delete[] ptr4_1;
	delete[] ptr4_2;
	delete[] ptr3;
	delete ptr2_1;
	delete ptr2_2;
	delete ptr1;

	return 0;
}

insert image description here

3.2 new and delete operation custom type

new and delete operate on custom types the same as on built-in types.

The difference between new/delete and malloc/free for custom types is that when applying for space: new will call the constructor, and when freeing space: delete will call the destructor

class A
{
    
    
public:
	A(int a = 0)
	{
    
    
		_a = a;
		cout << "A(int a = 0)" << " " << _a << endl;
	}
	~A()
	{
    
    
		cout << "~A()" << endl;
	}
private:
	int _a;
};

int main()
{
    
    
	A* ptr1 = new A(1);
	delete ptr1;

	A* ptr2 = new A;
	delete ptr2;

	A* ptr3 = new A[4];//调用四次构造函数
	delete[] ptr3;//释放四次构造函数

	A* ptr4 = new A[4]{
    
     1,2,3,4 };//为每次调用的构造函数传值
	delete[] ptr4;

	return 0;
}

insert image description here

For malloc and free, nothing will be called

class A
{
    
    
public:
	A(int a = 0)
	{
    
    
		_a = a;
		cout << "A(int a = 0)" << " " << _a << endl;
	}
	~A()
	{
    
    
		cout << "~A()" << endl;
	}
private:
	int _a;
};

int main()
{
    
    
	A* ptr1 = (A*)malloc(sizeof(A));
	if (ptr1 == nullptr)
	{
    
    
		perror("malloc fail!");
		exit(-1);
	}
	free(ptr1);

	A* ptr2 = (A*)malloc(sizeof(A)*4);
	if (ptr1 == nullptr)
	{
    
    
		perror("malloc fail!");
		exit(-1);
	}
	free(ptr2);

	return 0;
}

Advantage:

  1. more concise
  2. When applying for a custom type of space, new will call the constructor, delete will call the destructor, and malloc will not
  3. An exception will be thrown if new fails to apply for space, and malloc needs to judge whether the application is successful (some strict compilers must judge)

4. operator new and operator delete functions

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 operator new global function at the bottom to apply for space
  • delete uses the operator delete global function at the bottom to free up space.

We write the following code and view it through assembly

class A
{
    
    
public:
	A(int a = 0)
	{
    
    
		_a = a;
		cout << "A(int a = 0)" << " " << _a << endl;
	}
	~A()
	{
    
    
		cout << "~A()" << endl;
	}
private:
	int _a;
};

int main()
{
    
    
	A* ptr = new A;

	delete ptr;

	return 0;
}

insert image description here

  • Based on these, we know that new applies for space through operator new , and then calls the constructor to initialize.

As for delete, the VS2019 I use is inconvenient to view. You must understand that it calls operator delete at the bottom , and there is the following point.

  • Note: For objects of custom types, the destructor must be called first to clean up the data of the object, and then use operator delete to release the space. Otherwise, the destructor cannot be called if the space is released. (Built-in types do not have this requirement and are released directly)

Then let's take a look at how operator new and operator delete are implemented:

operator new:

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
			// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
			static const std::bad_alloc nomem;
			_RAISE(nomem);
		}
	return (p);
}

insert image description here

  • Observe line 5, operator new actually applies for space through malloc
  • Return directly when malloc successfully applies for space;
  • When the application fails and the user has set a measure for insufficient space , try to implement the measure and continue the application. If not set, an exception is thrown.

operator delete:

/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

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;
}
  • Observe lines 4 and 18, operator delete actually releases space through free

We can regard operator new and operator delete as encapsulation of malloc and free . The reason for doing this is because C++ is an object-oriented language. When an error occurs in the application space, it cannot handle the problem like C, and directly returns a NULL , need to allow users to see the emergence of problems more intuitively, and need to add the exception function. If you know some Java knowledge, you must be no stranger to exceptions, which is exactly the characteristic of object-oriented.

5. Implementation principle of new and delete

5.1 Built-in types

To apply for built-in type space, new and malloc, delete and free are basically similar, with the following two points of difference:

  1. new/delete applies for and releases the space of a single element, new[]/delete[] applies for and releases continuous space, malloc and free do not need to distinguish these
  2. When new fails to apply for space, an exception will be thrown, and malloc will return NULL

5.2 Custom types

  • 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
  • The principle of delete

    1. Execute the destructor on the space to complete the cleanup of resources 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 to apply for space

      In operator new[], the operator new function is actually called to complete the application of N objects.

    2. Execute the constructor N times on the requested space

  • The principle of delete[]

    1. Execute N times of destructors on the released object space to complete the cleanup of N object resources

    2. Call operator delete[] to release space

      Same as operator new[], operator delete is called in operator delete[] to release space.

6. Operators that must match

When we use operators to apply for space, we must match them, malloc/free, new/delete, new[]/delete[]

Look at each separately, what happens when there is a mismatch

6.1 Built-in types

int main()
{
    
    
	int* ptr1 = new int;
	free(ptr1);

	int* ptr2 = new int[10];
	free(ptr2);

	return 0;
}

We execute the above code and find that the compiler does not report an error, because new also applies for space through malloc and is released through free . For built-in types , we can directly use free to release without any problem.

6.2 Custom types

Define the following class, and the following code will use the following class to create the object application space (it will be explained where it is not used):

class A
{
    
    
public:
	A(int a = 0)
	{
    
    
		_a = a;
		cout << "A(int a = 0)" << " " << _a << endl;
	}
	~A()
	{
    
    
		cout << "~A()" << endl;
	}
private:
	int _a;
};

new – free

int main()
{
    
    
	A* ptr1 = new A;
	free(ptr1);

	return 0;
}

insert image description here

We have seen that the compiler will not report an error when such code is executed, but the destructor is not executed. This situation will be classified and discussed

  1. No resources that need to be released : just like this class A, it doesn’t matter whether it executes the destructor or not, because there is no additional application space in its object, we only need to release its new ones, which is free can be done.

  2. There are resources that need to be released : let's modify the above code and run it

    class B
    {
          
          
    public:
    	B(int a = 4)
    	{
          
          
    		_pa = new int[a];
    		cout << "B(int a = 0)" << endl;
    	}
    	~B()
    	{
          
          
            delete[] _pa;
    		cout << "~A()" << endl;
    	}
    private:
    	int* _pa;
    };
    
    int main()
    {
          
          
    	B* ptr = new B;
    	free(ptr);
    
    	return 0;
    }
    

    insert image description here

    Observe the above code, our free only releases the space pointed to by ptr , but the space pointed to by _pa in the object ptr is not released, which causes a memory leak , which is a very serious matter, and in C/C++, we apply for it ourselves The memory needs to be released by ourselves, the compiler will not manage it, and will not check for memory leaks , and it will still run normally.

    insert image description here

    Therefore, the order of delete is to call the destructor first , release the resources in the object, and then operator delete releases the space of the object.

    Let me give an example about the hazards of memory leaks: If a program is running all the time and it has a memory leak, it will consume space until the program cannot run, which is a serious problem.

    We use the following code to detect the maximum space that a program can open up (I use VS2019)

    int main()
    {
          
          
    	int size = 0;
    	while (1)
    	{
          
          
    		int* ptr = (int*)malloc(sizeof(int)*2*1024);
    		size += 1;//每次开辟1KB的空间
    		if (ptr == nullptr)
    		{
          
          
    			break;
    		}
    	}
    	cout << size / 1024 << "MB" << endl;
    	return 0;
    }
    

    insert image description here

Conclusion: New and malloc series, the underlying implementation mechanism is related to each other. There may be problems with mismatched use, or there may be no problem, so everyone must match and use.

expand:

If you have a certain understanding of java, you should know that java does not release memory, because it has a garbage collection mechanism called GC . When we create a new object, GC will record it. When we need to release the object after use, JVM (java virtual machine) will implement this mechanism to free up space.

As for why C++ does not use such a mechanism, because C/C++ is extremely performance-oriented, and mechanisms such as GC will affect performance to a certain extent, so C++ requires programmers to release it by themselves to improve performance.

From the perspective of language learning, GC is not a good thing. Although it is convenient to use, everyone who learns Java must understand the complex mechanism of GC .

new[]–free/new[]–delete

Observe the following two pieces of code and the running results:

Code 1:

int main()
{
    
    
	A* ptr = new A[4];
	free(ptr);

	return 0;
}

insert image description here

The compiler reports an error and cannot run normally.

Code 2:

int main()
{
    
    
	A* ptr = new A[4];
	delete ptr;

	return 0;
}

insert image description here

The compiler reports an error and cannot run normally.

The reason for the error reporting of these two pieces of code has nothing to do with the destructor. As mentioned in the previous paragraph, even if the destructor is not called, the worst is a memory leak. Compilation can still be executed, and the error will not be reported suddenly. The reason for the error lies in the development of new Mechanisms of continuous space

Let's first look at the correct way of writing:

	A* ptr = new A[4];
	delete[] ptr;

When using new to apply for continuous space, we filled in the number 4 in the square brackets after the type to tell the compiler that we need to apply for 4 A-type spaces and call the constructor 4 times, but when deleting, we did not tell the compiler How many times does the compiler call the destructor (the compiler will record how much space is freed, and when we use malloc to open up continuous space for built-in types, we don’t tell the compiler how much space is needed for free is proof), so how does the compiler know what to do? How many times is the destructor called?

insert image description here

When new opens up space, the compiler will open up 4 bytes of space forward at the start address of the opened space to store the number of destructors that need to be called (only used to store the number of calls to destructors, The compiler will use other methods to record the size of the released space), if the space is released, the writing method is, the delete[]released space will contain these 4 bytes, that is, when the space is released, the starting position pointed by the pointer is the four bytes The first address, so you know how many destructors need to be called. If the writing method is delete/free, the four bytes before the starting position will not be ignored. The position pointed by the pointer is the starting position of the requested space, just according to The steps go down (delete calls a destructor and then releases the space, free directly releases the space), the 4 bytes storing the number are reserved, and the pointer position of the freed space is wrong, which causes the compiler to report an error.

  • Generally, the crash of the program is caused by the problem of the pointer. This is because the initial position of the pointer is wrong. Like a memory leak, it will not crash.

One more thing to explain here is that when we defined class A, we wrote the destructor ourselves. If we didn’t write it ourselves, we will not report an error if we use new[]/fee, new[]/delete

class A
{
    
    
public:
	A(int a = 0)
	{
    
    
		_a = a;
		cout << "A(int a = 0)" << " " << _a << endl;
	}
	//~A()
	//{
    
    
	//	cout << "~A()" << endl;
	//}
private:
	int _a;
};
int main()
{
    
    
	A* ptr1 = new A[4];
	free(ptr1);
	A* ptr2 = new A[4];
	delete ptr2;

	return 0;
}

insert image description here

As above, when we annotate the destructor, there is only the compiler's own default destructor in the class. At this time, the compiler will think that there is no need to call the destructor, so there is no need to open up 4 bytes to store The number of times the destructor needs to be called, the compiler behaves normally.

Note: Again, because the grammar and underlying layer of C++ are more complicated, even if we know how to open up and release space, the compiler will not report an error, but it will eventually cause problems, such as our own mistakes, misleading others, etc., we still insist on compliance C++ syntax, use malloc/free, new/delete, new[]/delete[] together , do not cross use.

7. Locate the new expression

Positioning the new expression is to call the constructor to initialize an object in the allocated original memory space.

7.1 Usage format:

new(place_address)typenew(place_address)type(initializer_list)

place_address must be a pointer, initializer_list is the initialization list of the type

7.2 Usage scenarios:

As follows, we use malloc to apply for a space of a custom type. After the application, there is no way to automatically call the constructor for this space. We can only call the constructor initialization by locating new , and call the destructor directly when it is released. .

class A
{
    
    
public:
	A(int a = 0)
	{
    
    
		_a = a;
		cout << "A(int a = 0)" << " " << _a << endl;
	}
	~A()
	{
    
    
		cout << "~A()" << endl;
	}
private:
	int _a;
};

int main()
{
    
    
	A* ptr = (A*)malloc(sizeof(A));
	new(ptr)A(1);//定位new,传参
	//new(ptr)A;//定位new,不传参
	ptr->~A();//调用析构函数

	return 0;
}

insert image description here

Presumably everyone has doubts, why is it so complicated to write, it is not enough to just create a new object

	A* ptr = new A;
	delete ptr;

There must be some reason for its existence. We generally use new to apply for space from the heap of the operating system, but the operating system will be a little slow in allocating space, so if you need to frequently apply for memory in some places, there will be a memory pool . , used to store the space that needs to be allocated and the efficiency is higher than that of the operating system. At the same time, the memory allocated by the memory pool is not initialized . If it is an object of a custom type, it needs to be initialized with the definition expression of new.

7.3 Memory pool

Next, let's take a look at the workflow of the memory pool:

  1. When there is memory in the memory pool , we directly apply to it, and it will allocate space to us

    insert image description here

  2. When there is no memory in the memory pool , we directly apply to it, it will first apply to the operating system, tell the operating system that it has no memory, and the operating system will return a large amount of memory space to it , so that it will not be short of memory in a short time, Prevent efficiency decline.

    insert image description here

8. Interview questions

The following is an interview question. It is not recommended that you memorize it. It is best to understand and remember

The difference between malloc/free and new/delete

  1. malloc and free are functions, new and delete are operators
  2. The space requested by malloc will not be initialized, but new can be initialized
  3. When malloc applies for space, you need to manually calculate the size of the space and pass it on. New only needs to follow it with the type of space. If there are multiple objects, specify the number of objects in [].
  4. The return value of malloc is void*, which must be forcibly converted 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, NULL is returned, so the use must be judged as empty, 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 to complete before releasing the space Cleanup of resources in the space.

9. Summary and Blog Mind Map

For memory management, C/C++ is almost similar, but C++ has made more object-oriented adjustments on the basis of C

  1. Added new/delete
  2. Better calling constructors and destructors for custom types

insert image description here

Guess you like

Origin blog.csdn.net/m0_52094687/article/details/129035047