Detailed explanation of C language dynamic memory allocation

preface

Since the first two blogs were written, I feel that I have gained a lot. In particular, I review the knowledge I have learned again. I feel that I have a solid foundation, so I have formed a habit. After learning a module, I will write a blog. I not only write my own blog, but also want to share some of my opinions with you through this blog. Originally, I finished learning dynamic memory allocation on Thursday, but the blogger went to play at the weekend, so he forgot to write a blog. Now he works overtime to offer it.

1, Why is there dynamic memory allocation

1. Mastered memory development methods

In C language, we divide memory into four sections:
Code area, global variable and static variable area, local variable area, i.e. stack area, and dynamic storage area, i.e. heap area or free store area.

In order to facilitate your understanding, the diagram is as follows:

Through the previous study, we learned some memory usage methods:

(1) Create a variable
When we want to use a single variable, we can use memory by creating a variable.

int a = 10;//Local variable stack area
int g_a = 10;//Global variable - static area

(2) Create an array
When we need to use multiple variables of the same type, we can use memory by creating an array.

int arr[10];//Local variable stack area
int g_arr[10];//Global variable - static area

2. Characteristics of the above space development mode

The above two ways of using memory are what we have learned and commonly used, but in some cases, only these two methods are insufficient.

For example, when we need to create an array to store the student information of a class.

When we create this arr array, when we directly give the length of the array arr[50], this is very simple, but is it reasonable?
Example 1:

#include<stdio.h>
struct s
{
	char name[20];
	int age;
};
int main()
{
	struct s arr[50];// 50 struct s type data
	// 30: not enough
	// 60: Waste
	
	return 0;
}

If there are only 30 people in this class, do we waste part of the space; Suppose there are 60 people in this class, then the 50 we give is not enough. Therefore, it is unreasonable to give more or less here.

Someone here will say again. It's very simple: just give as much as you want! Like this
Example 2:

#include<stdio.h>
struct s
{
	char name[20];
	int age;
};
int main()
{
	int n = 0;
	scanf_s("%d", &n);

	struct s arr[n];//Error (active) 	 E0028 	 The expression must contain a constant value

	return 0;
}

The operation result is: error

Facts have proved that our ideas are wonderful, but the reality is cruel:
The error name here is: the expression must contain a constant value;
Description for struct s arr[n]; N here is a variable, so it won't work.

Here is an extension: Example 2 this code is called variable length array.
For variable length arrays, this writing method is currently only available for C99.

Summary: the characteristics of the above ways of opening up space
(1) The size of open space is fixed;
(2) When an array is declared, the length of the array must be specified, and the memory it needs is allocated at compile time.

3. Why is there dynamic memory allocation

Our demand for memory development space is not limited to these methods. Sometimes the size of space we need can be known when the program is running. At this time, the above methods can not achieve the purpose, so dynamic memory allocation came into being.

2, Introduction to dynamic memory functions

1,malloc

C language provides a function for dynamic memory development:

	void* malloc (size_t size);

malloc's full name is memory allocation, which is called dynamic memory allocation in Chinese. It is used to apply for a continuous memory block area of a specified size and return the address of the allocated memory area in void * type. When the specific location of memory cannot be known and you want to bind the real memory space, you need to use dynamic memory allocation, and the allocated size is the size required by the program.

This function requests a continuously available space from memory and returns a pointer to this space.
(1) If the development is successful, a pointer to the developed space is returned;
(2) If the development fails, a NULL pointer is returned, so the return value of malloc must be checked;
(3) The type of the return value is void *, so the malloc function does not know the type of open space, which is determined by the user when using it;
(4) If the parameter size is 0, malloc's behavior is standard and undefined, depending on the compiler.

Take an example
Example 3:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	// Request 10 shaping spaces from memory
	int* p = (int*)malloc(10 * sizeof(int));
	// malloc -> #include<stdlib.h>

	//int* p = malloc(10 * sizeof(int));
	// error 	  C2440 	  'initialization': cannot convert from 'void *' to 'int *
	if (p == NULL)
	{
		// One way to print the cause of an error
		printf("%s\n", strerror(errno));
		// strerror -> #include<string.h>
		// errno -> #include<errno.h>
	}
	else
	{
		// Normal use space
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	return 0;
}

The operation result is:
0 1 2 3 4 5 6 7 8 9

In example 3, if we use the method of "int* p = malloc(10 * sizeof(int));" to open up the space, errors will be reported in most compilers with strict detection because of different variable types. From here, we can also see that the return value of malloc open up space is of void * type;

We also mentioned above: if malloc fails to develop, a NULL pointer will be returned, so the return value of malloc must be checked. Therefore, we use a special way to print the error cause - "printf("%s\n", strerror(errno));". In this way, if the development fails, the compiler will not report an error, but print the error cause after running.

Error prone tips:
Because the memory of our computer is also limited, we can't open up space at will. When the space we need to open up is not enough, the printing error will appear "Not enough space".

2,free

Immediately above, we can't open up space at will. Because the space is limited, we should borrow and return. We borrowed so much memory from the system in front. When we run out of memory, we should return this memory to the system, so how can we return it? Here we need to use our free function.

C language provides us with another function for dynamic memory release and recovery:

	void free(void *ptr)

The free function is used to free dynamic memory:
(1) If the space pointed to by the parameter ptr is not dynamically opened up, the behavior of the free function is undefined;
(2) If the parameter ptr is a NULL pointer, the function does nothing.

Let's start with an example
Example 4:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	// Request 10 shaping spaces from memory
	int* p = (int*)malloc(10 * sizeof(int));
	// malloc -> #include<stdlib.h>

	//int* p = malloc(10 * sizeof(int));
	// error 	  C2440 	  'initialization': cannot convert from 'void *' to 'int *
	if (p == NULL)
	{
		// One way to print the cause of an error
		printf("%s\n", strerror(errno));
		// strerror -> #include<string.h>
		// errno -> #include<errno.h>
	}
	else
	{
		// Normal use space
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	// When the dynamically applied space is no longer used
	// It should be returned to the operating system
	free(p);
	p = NULL;
	return 0;
}

The operation result is:
0 1 2 3 4 5 6 7 8 9

For scale 3 and example 4, example 4 has only two lines of code:
free( p );
p = NULL;

Some people will wonder. The running results of example 3 and example 4 are obviously the same. Why should we add these two lines of code?
Yes, it seems that the running results are the same, but this is only for the case that we have a small amount of code, and the memory we applied for is enough, so the purpose is achieved; But if we want to do a project with a huge amount of tasks, we can only borrow it but not return it. The memory of the system is decreasing. Can we continue to write programs? Therefore, we should form a habit from now on. After the applied memory is used up, we must carry out the free () operation.

Someone here has another question. Wouldn't it be good if we ran out of memory and released it? Why make this pointer p null?
In fact, after our free (P) operation is completed, this space is released, but the value of P has not changed. If someone finds this p and destroys it, our program may have problems. Therefore, we might as well take the initiative to set P as a null pointer to let people who want to cut off these thoughts.

Speaking without practicing is a big taboo in learning programming language. Let's strike while the iron is hot and do an exercise:

The correct answer is:
Example 5:

#include "string.h"
#include <stdio.h>
#include<stdlib.h>
int main()
{
	char* src="hello,world"; 
	char* dest=NULL;
	int len=strlen(src);
	dest=(char*)malloc(len+1);// To allocate space for \ 0
	char* d=dest;
	char* s=src+len-1;// Point to the last character
	while(len--!=0){ 
		*(d++)=*(s--);// Be careful not to lose the * sign
		*d ='\0';// Don't forget '\ 0' at the end of the string
	} 
	printf("%s",dest);
	free(dest);// Free up space after use to avoid memory leakage
	dest = NULL; // Releasing is not equal to safety, and the operation of setting it as a null pointer cannot be omitted
	return 0;
}

3,calloc

C language also provides a function called calloc, which is also used for dynamic memory allocation:

	void* calloc(size_t num,size_t size)

(1) The function is to open up a space for num size elements, and initialize each byte of this space to 0;
(2) The difference between calloc and malloc is that calloc initializes each byte of the requested space to all 0 before returning the address

for instance:
Example 6:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	int* p = (int*)calloc(10 , sizeof(int));
	// calloc -> #include<stdlib.h>

	if (p == NULL)
	{
		// One way to print the cause of an error
		printf("%s\n", strerror(errno));
		// strerror -> #include<string.h>
		// errno -> #include<errno.h>
	}
	else
	{
		// Normal use space
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	// When the dynamically applied space is no longer used
	// It should be returned to the operating system
	free(p);
	p = NULL;
	return 0;
}

The operation result is:
0 0 0 0 0 0 0 0 0 0

It can be seen that the calloc function initializes each byte of the dynamic space to 0

4,realloc

Back to today's core problem, what if we need to adjust the size of memory in the process of using memory?

C language also provides us with a function called realloc, which can make dynamic memory management more flexible:

	void* realloc(void* ptr, size_t size);

(1) ptr is the memory address to be adjusted;
(2) Size is the adjusted new size;
(3) The return value is the adjusted memory starting position;

for instance:
Example 7:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(20);
	for (int i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", *(p + i));
	}
	int* p2 = (int*)realloc(p, 40);
	for (int j = 5; j < 10; j++)
	{
		*(p + j) = j;
	}
	for (int j = 5; j < 10; j++)
	{
		printf("%d ", *(p + j));
	}
	free(p);
	p = NULL;
	return 0;
}

The operation result is:
0 1 2 3 4 5 6 7 8 9

(4) This function will also move the data in the original memory to a new space on the basis of adjusting the size of the original memory space;

(5) realloc has two situations in the process of adjusting memory space
① There is enough space behind the original space
At this time, space is added directly after the original memory, and the data in the original space does not change.

② There is not enough space after the original space
Find another continuous space of appropriate size on the heap space to use, so that the function returns a new memory address.

The diagram is as follows:

3, Common dynamic memory errors

1. Dereference operation on NULL pointer

Example 8:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	// 1. Dereference NULL pointer
	int* p = (int*)malloc(40);
	// In case malloc fails, p is assigned NULL
	// unsafe
	// Remember to judge whether p is empty
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;

	return 0;
}

For example 8, if malloc fails to open up space, p is assigned NULL, and the following operation is performed on the NULL pointer, * (p + i) is always an illegal address, and our operation is always an illegal operation, so we must remember to judge whether p is empty before use.

2. Out of bounds access to dynamic memory

Example 9:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	// 2. Cross border access to dynamic memory
	int* p = (int*)malloc(40);// 10 ints - > 0-9
	if (p == NULL)
	{
		return 0;
	}
	int i = 0;
	// Cross the border
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

For example 9, we used malloc to apply for 10 int types from the system, but we accessed 11 int types later. When running the program, there will be false death. Although it is dynamic memory, it also has boundaries. Once the access exceeds the boundaries, the program will have problems.

3. Use free release for non dynamic memory

Example 10:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	// 3. Use free to release non dynamic memory
	int a = 10;
	int* p = &a;
	free(p);
	p = NULL;
	return 0;
}

For example 10, the space of a is stored in the stack area. It is not a dynamically opened space. The free function must release the space opened on the heap area. If free is used for non dynamically opened memory, the program will fake death.

4. Use free to release part of dynamic memory

Example 11:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	// 4. Use free to release a part of dynamic memory
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 0;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p++ = i;
	}
	// Recycle space
	free(p);
	p = NULL;
	return 0;
}

For example 11, we have such an operation "* p++ = i;". When this operation ends, the space pointed to by our pointer p is no longer the complete space we dynamically open up, not limited to pointing to the end. As long as p here no longer points to the initial position of the space, the program will crash.

5. Multiple releases of the same dynamic memory

Example 12:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	// 5. Multiple releases of the same dynamic memory
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 0;
	}
	// Suppose space is used
	// release
	free(p);
	// ... after a lot of lines of code 
	free(p);// Release again 
	return 0;
}

For example 12, after using the space, we released the space. After many lines of code, we released the space again. In this way, the program will also fake death. How can we improve it?
Example 13:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	// 5. Multiple releases of the same dynamic memory
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 0;
	}
	//You can
	free(p);
	p = NULL;
	free(p);
	return 0;
}

As in example 13, each time the space is released, take the initiative to set p as a null pointer, which can effectively avoid the above situation, because we mentioned earlier:
For the free function: if the parameter ptr is a NULL pointer, the free function does nothing.

6. Dynamic memory forget release (memory leak)

Example 14:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	// 6. Dynamic memory forgetting to release (memory leakage)
	while (1)
	{
		malloc(1);//warning 	 C6031 	 The return value is ignored: "malloc".
	}
	return 0;
}

For example 14, when we forget to release the memory, it will cause a memory leak. Our computer may crash. In this case, we usually restart, but when we write tens of thousands of lines of programs, it will be a very terrible thing.

4, Several classic written test questions

1. Topic 1

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

What will be the result of running the Test function?
The answer is: program crash

For this topic, many people will focus on "printf(str);". In fact, there is no problem here. It is equivalent to "printf("%s\n",str);".

Resolution code:
When you see "GetMemory(str);", what we pass here is the value of STR itself, not the address of str. after entering the GetMemory function, we open up 100 spaces on the heap. We put these spaces in P. here P is used as a formal parameter variable. After the GetMemory function is completed, this p is destroyed. In fact, STR is still NULL, Next, we want to copy "hello world" to STR, but STR, as NULL, does not point to an effective space. During the operation, illegal access cannot be avoided. Although there is no problem with the subsequent printf operation, the program has crashed during the strcpy operation.

Summary:
(1) Running the code program will crash;
(2) The program has a memory leak:
str is passed to p as a value
p is the formal parameter of GetMemory function, which is only valid within the function
After the GetMemory function returns, the dynamic memory has not been released
And can not be found, so it will cause memory leakage

2. Topic 2

"Return stack space address problem"

char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

What will be the result of running the Test function?
The answer is: random value (or crash)

Resolution code:
When you see "str = GetMemory();", when you enter the GetMemory function, p [] this array is a formal parameter in the GetMemory function. It applies for a space, which only exists in the GetMemory function. When the GetMemory function ends, the address of p is indeed returned and placed in str, but after the GetMemory call is completed, p the space created by this array is returned to the operating system. We don't know the value stored in this space. Next, "printf(str);"
We don't know the printed value, so the result is a random value.

3. Topic 3

void* GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}

What will be the result of running the Test function?
The answer is:
(1) Output hello
(2) But there is a memory leak

Resolution code:
See "GetMemory (& STR, 100);", pass the str address into the GetMemory function and receive it with the secondary pointer p, then the address pointed to by * p is STR, and then copy "hello" to STR and print it out. These operations are no problem, but after using STR, we forget to release the dynamically opened memory, resulting in memory leakage.

4. Topic 4

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

What will be the result of running the Test function?
The answer is:
(1)world
(2) Illegal access to memory (tampering with the contents of dynamic memory area, the consequences are difficult to predict and very dangerous)

Resolution code:
First, we applied to the system for 100 bytes, and the address is stored in str; Then, we copy "hello" into str; Next, we release this space, and then the space pointed to by STR has been returned to the operating system; Then, judge whether STR is a null pointer. Although we released the applied dynamic memory before, the value of STR has not changed. It is still "hello", so it is not a null pointer; After entering the if statement, copy "world" into STR, and world will overwrite hello; So after printing STR, the result is world.

Although the world is printed, there is still a problem with this program. For the "free(str);" operation, the space has been released, which indicates that this space does not belong to us and we can no longer use this space. However, next, we put the world in and print, which belongs to illegal memory access.

Tip: free (p) and p=NULL must be used consistently!

summary

That's the end of the explanation on dynamic memory allocation. Dynamic memory allocation is not difficult. It's more about reciting some concepts. As long as we remember these error prone points and grasp them, it's still easy! Come on, go, go!
ps: dynamic memory allocation has been delayed for quite a long time. For blogs about files, bloggers will be in a hurry

Tags: C Algorithm

Posted on Mon, 22 Nov 2021 19:01:57 -0500 by yuraupt