[advanced C language] from entry to penetration (dynamic memory management)

Keywords: C data structure

preface:
In c/c + + language, sometimes the program can not determine how large the array should be defined, so it is necessary to dynamically obtain more memory space from the system when the program is running. So today we'll learn how dynamic memory allocation works.

1, Why is there dynamic memory allocation

Let's look at this Code:

int main()
{
	int a = 10;// Open up four bytes in stack space
	int arr[10] = { 1,2,3 };// Open up 10 bytes of continuous space on the stack space
	return 0;
}

Some students may say: what? I'm all in. Can you show me this? Yes, just show you this. If we take a closer look, we can see that int a occupies 4 bytes, while arr array occupies 40 bytes, but only 12 bytes are used to store data. Therefore, it is a waste of space without making changes for the time being.

The above way of opening up space has two characteristics:

  1. The size of the space is fixed.
  2. When declaring an array, you must specify the length of the array, and the memory it needs will be allocated at compile time.

But the demand for space is not just the above situation. Sometimes the size of the space we need can only be known when the program is running, and the way of opening up space during array compilation can not be satisfied. At this time, you can only try dynamic memory development. So we have the concept of dynamic memory allocation:

The so-called dynamic memory allocation refers to the method of dynamically allocating or reclaiming storage space in the process of program execution.

In fact, the memory occupied by a program compiled by C/C + + is divided into the following parts

1. Stack - automatically allocated and released by the compiler to store the parameter names of functions, local variable names, etc. Its operation is similar to the stack in the data structure.

2. Heap - allocated and released by the programmer. If the programmer does not release it, it may be recycled by the OS at the end of the program. Note that it is different from the heap in the data structure, and the allocation method is similar to the linked list.

3. Static - Global and local static variables are stored together. Released by the system at the end of the program.

4. Text constant area - the constant string is placed here and released by the system after the program is completed.

5. Program code area - the binary code that holds the function body.

Our dynamic memory allocation exists in the heap area and has several dynamic memory functions to maintain. Next, we will introduce each function:

2, Introduction to dynamic memory functions

1.malloc

Let's go first cplusplus Let's take a look at the definition of malloc:

void * malloc(size_t size)

In fact, malloc is a C language that provides a function for dynamic memory development. This function applies for a continuously available space from memory and returns a pointer to this space. Note that the returned pointer is of type void *.

So we can define a function and try:

int main()
{
	int * p = (int *)malloc(40);
    //Apply for 40 bytes of space from memory and force the return pointer to int* 
	return 0;
}

Because the return pointer of malloc is void *, if we receive directly, it is untyped, so if we intend to store the integer, we will force the type to be converted to the pointer storage of int *. We also note the instructions in malloc:

1. If the development is successful, a pointer to the developed space will be returned.
2. If the development fails, a NULL pointer will be returned. Therefore, 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, the malloc behavior is standard and undefined, depending on the compiler.

Therefore, we need to check the return value after malloc development and select the type:

int main()
{

	int * p = (int *)malloc(40);
	if (p == NULL)
	{
		return -1;//Judge whether the development is successful
	}

	int i = 0;
	for (i = 0; i < 10; i++)//initialization
	{
		*(p + i) = i;
	}
	return 0;
}

2.free

Since there is application space, we need to free up space, so we have the free function:

C language provides another function free, which is specially used for dynamic memory release and recovery. The function prototype is as follows:

void free (void* ptr);

Simply put: the free function is used to release dynamically opened memory.

Then, in the above code, after we have applied and successfully developed, we will use this space, and we will release this memory space:

#include <stdlib.h>

int main()
{

	int * p = (int *)malloc(40);
	if (p == NULL)
	{
		return -1;//Judge whether the development is successful
	}
	int i = 0;
	for (i = 0; i < 10; i++)//initialization
	{
		*(p + i) = i;
	}

	free(p);
	p = NULL;//be careful!
    //free releases the space that P points to, but p still points to this space, which is very dangerous
    //Therefore, it is necessary to change p to NULL after free frees up space.
	return 0;

At the same time, the free function also has some points to pay attention to:

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.

Therefore, when we use the free function, we cannot point to a space that is not dynamically opened up. And malloc works better with free.

3.calloc

Let's learn the calloc function again:

In fact, the calloc function also opens up dynamic memory space. What's the difference between calloc and malloc? Let's look at definitions and parameters:

void* calloc (size_t num, size_t size);

The difference between malloc and malloc is that there are two parameters here. We can create several memory units, how much space is allocated to each unit, and more clearly allocate space. But the main difference between malloc and calloc is:

malloc function is only responsible for applying for space in the heap area and returning the starting address without initializing the memory space.
The calloc function applies for space in the heap area, initializes to 0, and returns the starting address.

#include <errno.h>
#include <string.h>

int main()
{
	int* p = (int *)calloc(10, sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));//Error reporting function
		return -1;
	}

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
        //Print result: 0 0 0 0 0 0 0 0 0 0 0
	}

	free(p);
	p = NULL;
	return 0;
}

If our parameters are very large, the requested memory cannot be successfully applied, resulting in an error:

int main()
{
	int* p = (int *)calloc(1000000000, sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

	free(p);
	p = NULL;
	return 0;
}

Therefore, if we require initialization of the requested memory space, we can easily use the calloc function to complete the task. In other words, when we need to initialize, we use the calloc function, and we use the malloc function without initialization.

4.realloc

So how to show the dynamic? It is shown in the realloc function. The emergence of realloc function makes the dynamic memory management more flexible:

Sometimes we find that the space applied for in the past is too small, and sometimes we feel that the space applied for is too large. For the sake of reasonable memory, we will adjust the size of memory flexibly. The realloc function can adjust the size of dynamic memory.

int main()
{
	int* p = (int *)calloc(10, sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

	int* ptr = (int*)realloc(p, 20 * sizeof(int));//modify
	//Increase space to 20 int s
	p=ptr;
	free(p);
	p = NULL;
	return 0;
}

realloc should pay attention to:

1.ptr is the memory address to be adjusted
2. New size after size adjustment
3. The return value is the adjusted memory starting position.
4. On the basis of adjusting the size of the original memory space, this function will also move the data in the original memory to a new space.

Therefore, there are two situations when realloc adjusts the memory space:

Case 1: there is enough space behind the original space

In case 1, to expand the memory, directly add space after the original memory, and the data in the original space will not change.

Case 2: there is not enough space after the original space

In case 2, when there is not enough space after the original space, the expansion method is to find another continuous space of appropriate size on the heap space. In this way, the function returns a new memory address. Due to the above two cases, we should pay attention to the use of realloc function.

Therefore, when realloc adjusts the dynamic development memory size, if the modification and increase space is too large to increase, it will also lead to code bug s. Therefore, we should also consider that when realloc modification fails, the return value of the failure is also NULL, so our previous code should be modified as follows:

int main()
{
	int* p = (int *)calloc(10, sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));

	}

	int* ptr = (int*)realloc(p, 20 * sizeof(int));
	if (ptr != NULL)//Add a judgment
	{
		p = ptr;//If ptr is empty, p can also point to the original address
	}
	else
	{
        return -1;//If it is empty, exit and do not continue
    }
	
    for(i = 10;i < 20; i++)//At the same time, we can also initialize the 10 bits added later
    {
     * (p + i) = i;
    }
    for(i=0;i<20;i++)
    {
        printf("%d ",*(p + i));
    }

	free(p);
	p = NULL;
	return 0;
}

These are several functions of dynamic memory allocation.

3, Common dynamic memory errors

There are some common errors in dynamic memory allocation. Next, let's analyze several common errors and precautions.

1. Dereference of NULL pointer

The first is not to detect whether the pointer is NULL, and then directly dereference.

int main()
{
	int* p = (int*)malloc(40000000000);
	//If p is a null pointer, the following code is problematic
	
	*p = 0;
	//Writing code like this is risky!!
	return 0;
}

A C6011 warning is generated under vs2019, which indicates that the code dereference may be null
Pointer to. If the value of the pointer is invalid, the result is undefined.

So we must remember to add a judgment:

int main()
{

	int* p = (int*)malloc(40000000000);
	if (p == NULL)//Increase judgment
		return -1;
	*p = 0;
	
	return 0;
}

2. Cross border visits to dynamic open spaces

The cross-border access to the dynamic development space means that the memory we access exceeds the memory we have developed, resulting in cross-border access.

Like the following code, the memory space we open up is 40, that is, the size of 10 shaping, but when we access 20 elements during assignment, it will cause cross-border access:

void test()
{
	int i = 0;
	int* p = (int*)malloc(40);
	if (NULL == p)
	{
		return -1;
	}
	for (i = 0; i <= 20; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;

	return 0;
}

The code will hang up when it runs. If it doesn't hang up, it only means that the compiler doesn't detect it this time, but in principle, the code is still wrong.

3. Use free to release non dynamic memory

As mentioned earlier, the free function releases the dynamically developed memory when it is released, but you can't use free to release the non dynamically developed memory.

For example:

int main()
{
	int a = 0;
	int* p = &a;
	//p don't want to use it

	free(p);
	p = NULL;

	return 0;
}

In this way, the subcode will also hang up directly.

4. Use free to release a part of dynamic memory

When free releases a piece of dynamic development memory, it should be released as a whole, but it may also cause code errors due to some of our errors. For example, it releases a part of a piece of dynamic development memory:

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return -1;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*p++ = i;
	}
	//After the above code + +, p no longer points to the starting space
	free(p);
	p = NULL;

	return 0;
}

Except for partial release, no release, release error, etc. are all wrong.

5. Release the same dynamic memory multiple times

Similarly, it is also wrong to release a piece of dynamic memory multiple times, such as:

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return -1;
	}

	free(p);
	free(p);//Release err multiple times
	p = NULL;

	return 0;
}

However, if p is set as a null pointer after release, there will be no problem even if it is released again, so we must remember to set p as a null pointer after free release. For example:

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return -1;
	}

	free(p);
	p = NULL;
	free(p);//p is already a null pointer, and there is no problem releasing it again
	p = NULL;

	return 0;
}

6. Dynamic memory forgetting to release (memory leakage)

There are two ways to release dynamic memory:

1.free active release
2. When the program exits, the applied space will also be recycled.

For example:

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return -1;
	}
	getchar();
    //No memory free

	return 0;
}

When we do not release memory after use, it will cause a waste of memory. If a large server is running, such as king glory, it can only stop when it is being maintained, and the wasted memory space may lead to less smooth operation.

So:

Forgetting to free dynamic space that is no longer in use can cause memory leaks. Remember: the dynamic space must be released and released correctly.

4, Several classic written test questions

1. Topic 1

What will be the result of running the Test function?

void GetMemory(char *p)
{
 p = (char *)malloc(100);
}

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

int main()
{
 Test();
 return 0;
}

Let's demonstrate the code running sequence:

The answer is: the program will crash because of memory leaks and illegal access.

Parsing: in fact, there is no error before we enter the Test function and create the str variable, and then there is no error when passing the str value to the parameter GetMemory. But when it gives the address of the space opened by malloc to p, an error occurs. This is because:

1. When STR is passed to p, the value is passed, and p is the temporary copy of str. therefore, when the starting address of the space opened by malloc is placed in p, STR will not be affected, and STR is still NULL.

Then, the str will not actually change, so if you copy it when it is still NULL:

2. When str is NULL and strcpy wants to copy hello world to the space pointed to by str, the program will crash. Because the space pointed to by NULL cannot be accessed directly.

Then we turn our attention back to p. here, do we not release malloc after it is opened, and return to the Test function after it is stored, which means that p has been destroyed with GetMemory, and we can't even find this space (p is destroyed and has no direction):

3. After using the dynamic memory, forgetting to release the dynamic space that is no longer used will cause memory leakage.

Then we remedy and modify this code so that it has no problem. In fact, there are two problems here. One is to pass the value of the parameter, and the other is that there is no free space, so we can pass the address and release it:

//Solution: pass the address
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}

void Test(void)
{
	char* str = NULL;
	GetMemory(*str);//Pass the address
	strcpy(str, "hello world");
	printf(str);

	//Release after use
	free(str);
	str = NULL;
}

int main()
{
	Test();
	return 0;
}

2. Topic 2

What will be the result of running the Test function?

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

void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

int main()
{
	Test();
	return 0;
}

Similarly, let's analyze the running order of the code:

Answer: Hot pound Z (random value)

Resolution:

Here, the code actually makes an illegal access and unsafe access error. After the GetMemory function is executed, the returned pointer p is stored in str. at this time, the function GetMemory has been destroyed, that is, the constant string created by char p [] has been destroyed. At this time, even if the address of p is remembered, the content of the string is not accessed, so the random value is obtained when accessing.

A vivid example:

I was in a bad mood today. I went out to stay in a hotel, and then I lived in Room 305 (creating memory space). Then I thought that Zhang San was to blame for my bad mood. Then I called Zhang San (assigning the created address to str) and said that I had opened a five-star hotel, but I wouldn't stay tomorrow, and then I asked him to stay. After hearing this, Zhang San was overjoyed (received the address) and brought all his family's things the next day (visited the address). But I left my room early in the morning and went back to school (release the address space). Then Zhang San was not happy there. He kept screwing the 305 room and wanted to go in. He bumped and hit and finally went in, but this behavior was wrong (illegal access to memory), and then he was arrested by the police uncle.

Similar codes include:

int* fun()
{
	int n = 10;
	return &n;
}


int main()
{
	int* str = fun();
	printf("&d", *str);
	
	return 0;
}

The above code may print 10, but it is still wrong in essence. It can be printed only to indicate that although the function is destroyed, the space for 10 has not been overwritten and modified, so when the address is found, print 10. But if the code is copied a little more, it may be overwritten:

//Become a random value
int* fun()
{
	int n = 10;
	return &n;
}


int main()
{

	int* str = fun();
	printf("123\n");//Add something by the way

	printf("%d", *str);
	return 0;

}

3. Topic 3

What will be the result of running the Test function?

void GetMemory(char **p, int num)
{
 *p = (char *)malloc(num);
}

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

This question is more watery. If we can do all the previous questions, this is just an error that does not release memory in the end. We only need to print f (STR) after using it; Just add free(str) and str = NULL.

4. Topic 4

What will be the result of running the Test function?

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

int main()
{
	Test();

	return 0;
}

The analysis of this problem is directly pulled in the figure. In fact, because the space is released but the pointer is not set to NULL, the wild pointer continues to illegally access memory. Therefore, after releasing the dynamic memory, remember to modify the pointer to NULL.

5, Flexible array

Perhaps you have never heard of the concept of flexible array, but it does exist. In C99, the last element in the structure is allowed to be an array of unknown size, which is called a flexible array member.

That is, the last element in the structure is an array, and the size may not be specified, for example:

struct fun
{
 int i;
 int a[0];//Flexible array member
}
//Or it can be blank directly
struct fun2
{
 int i;
 int a[];//Flexible array member
}

Flexible arrays have the following characteristics:

1. The flexible array member in the structure must be preceded by at least one other member.

2. The size of this structure returned by sizeof does not include the memory of the flexible array.

3. The structure containing flexible array members uses malloc() function to dynamically allocate memory, and the allocated memory should be larger than the size of the structure to adapt to the expected size of the flexible array.

First, there must be at least one member. For example, this is not possible:

 struct fun3
{
	int a[];//There are no members ahead, err
};

Then, the structure size returned by sizeof does not include the memory of the flexible array, that is, we do not calculate the memory of the flexible array when calculating the memory, for example:

struct fun
{
	int i;//4
	int a[];//Flexible array member
};

int main()
{
	printf("%d", sizeof(struct fun));
    //The result is 4
	return 0;
}

Here, our structure has two members of i and a arrays, but only 4 bytes of i are calculated. At the same time, it is also the result of memory alignment. In the third point, the structure containing flexible array members is used for dynamic memory allocation with malloc() function. Let's look at the code:

#include <string.h>
#include <errno.h>

struct fun
{
	int i;
	int a[];//Flexible array member
};

int main()
{
	struct fun* ps = (struct fun*)malloc(sizeof(struct fun) + 10 * sizeof(int));
	//What does this code mean?
		if (ps == NULL)
		{
			printf("%s\n", strerror(errno));
		}
	return 0;
}

Let's analyze this Code:
struct fun* ps = (struct fun*)malloc(sizeof(struct fun) + 10 * sizeof(int));

First of all, when we create the structure of flexible array, the life is not directly like struct fun ps, which needs to be used with malloc. Then, the initial size is sizeof(struct fun), so malloc(sizeof(struct fun)) is the size of the structure that does not contain the flexible array, and then the size of the flexible array is followed. For example, I want to create 10 arrays of integer size, followed by 10 * sizeof(int). Since malloc is created, there should be pointer reception, So we used the struct pointer struct fun* ps to receive it, and then malloc started to be typeless, so we should first convert its forced type to struct fun* ps, and then assign it. So you get a flexible array.

Since the explanation says flexibility, let's write it completely and see how it is flexible:

//Method 1
int main()
{
	struct fun* ps = (struct fun*)malloc(sizeof(struct fun) + 10 * sizeof(int));
		if (ps == NULL)
		{
			printf("%s\n", strerror(errno));
			return -1;
		}
		//Successful development
		ps->i = 100;
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			ps->a[i] = i;
		}

		//Array a space is not enough, adjust memory
		struct fun* ptr = (struct fun*)realloc(ps,sizeof(struct fun) + 20 * sizeof(int));
		if (ptr == NULL)
		{
			printf("Failed to expand space\n");
			return -1;
		}
		else
		{
			ps = ptr;
			//Continue to use
			for (i = 10; i < 20; i++)
			{
				ps->a[i] = i;
			}

			//Print view
			for (i = 0; i < 20; i++)
			{
				printf("%d ", ps->a[i]);
			}
		}

		//release
		free(ps);
		ps = NULL;
		return 0;
}

Here we also use malloc and realloc functions to make it flexible, that is, dynamically adjust the memory. When the memory of the array changes, if you want an array but are not sure of its size, you can use a flexible array.

In fact, we can also use pointer pointing array to the concept of flexible array. Here, we first create pointer member variables in the structure, and then dynamically expand the space pointed to by the pointer, which can also achieve the concept of flexible array. However, at the same time, the space we open and the space we increase is equivalent to two dynamic memories, So release it twice:

//Method 2
struct fun
{
	int i;
	int *a;
};

int main()
{
	struct fun* ps = (struct fun*)malloc(sizeof(struct fun));
	if (ps == NULL)
	{
		printf("%s\n", strerror(errno));
		return -1;
	}
	//Successful development
	ps->i = 100;
	ps->a = (int*)malloc(10 * sizeof(int));

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->a[i] = i;
	}

	//Array a space is not enough, adjust memory
	int* ptr = (int*)realloc(ps->a, 20 * sizeof(int));
	if (ptr == NULL)
	{
		printf("Failed to expand space\n");
		return -1;
	}
	else
	{
		ps = ptr;
		
	}

	//release
	free(ps->a);
	ps->a = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

The above two methods can realize such functions, but the implementation of method 1 has two advantages:

The first advantage is: easy memory release
The second advantage is that this is conducive to access speed

1. If our code is in a function for others, you make a secondary memory allocation in it and return the whole structure to the user. The user can release the structure by calling free, but the user does not know that the members in the structure also need free, so you can't expect the user to find it. Therefore, if we allocate the memory of the structure and the memory required by its members at one time and return a structure pointer to the user, the user can free all the memory once.

2. Continuous memory is beneficial to improve access speed and reduce memory fragmentation. Because the first method is that the memory is stored continuously, and the second method creates different spaces, the first method improves the access speed and reduces scattered memory fragments.

Well, that's all for this article. That's all for dynamic memory management. If you have any problems, please pay attention to each other and make common progress.

There is another thing:

Posted by BostonMark on Fri, 08 Oct 2021 16:43:09 -0700