Introduction to C language memory management

Posted by r3drain on Sun, 26 Dec 2021 19:52:19 +0100

What is memory

Macroscopically, the memory that stores data can be called memory. The memory discussed in this chapter is the memory for the program in program design.

Memory is used to store data, which can be understood as arranged in boxes. Boxes are used to store data, but the boxes themselves have numbers and are continuous. This number is the memory address.

That is, memory can be abstracted into data and addresses.


For example, BB in the figure above is stored in the address 0x00000002. As long as you access this address, you can get this data. On a 32-bit machine, the program can get up to 4GB of continuous memory when running. This part of memory is called the program's virtual memory.

Why 4GB?

Every 8 binary bits is a byte. Byte is the smallest unit of memory. Each byte has its own address. 32 bits refer to 32 binary bits to represent the address. Through the calculator, we can know that the minimum 32 bits is 32 zeros and the maximum is 32 ones, that is, there are a total of 32 bits 2 32 + 1 2^{32}+1 232 + 1 addresses can mean that so many addresses represent so many bytes, that is, 4294967296 bytes.

So, according to the conversion relationship, that is, the size of 4GB.

This explains why no matter how many memory modules are inserted on a 32-bit machine, the maximum can only display 4GB. Because the CPU has no way to represent a memory address greater than 4GB.

If it is a 64 bit program, it can use 64 bits to represent the address, which is a very large address. This paper only discusses the case in the 32-bit environment.

Four memory areas

Memory is divided into four areas according to function, from low to high, which are stack area, heap area, code area and data area.


This article will discuss the memory management of stack and heap in detail. But there is no need to struggle with their specific meaning and usage.

Stack area

What is a stack?

In Chinese, the original meaning of inn refers to shed or livestock shed, which is later extended to houses for storing goods or accommodating passengers, such as Inn and plank house. Their characteristics are in and out. It also has the same characteristics in the concept of computer. The original meaning of Stack in English is:

A stack of things is a pile of them.

a pile of is more consistent with the image of data in memory, because stacked things must be stacked from the bottom and taken from the top, which is very consistent with the operation of stack.


The operation of stacking data is stack operation, which is put in from the bottom and then taken out from the top.

The computer does the same. The stack stores the local variables we define, so we can do an experiment through the local variables.

#include<stdio.h>
#define PrintVarName(x) #x
int main() {
	unsigned char c1, c2, c3, c4;
	c1 = 0xaa;
	c2 = 0xbb;
	c3 = 0xcc;
	c4 = 0xdd;
	printf("Variable %s = %.2x at %p\n", PrintVarName(c1), c1, &c1);
	printf("Variable %s = %.2x at %p\n", PrintVarName(c2), c2, &c2);
	printf("Variable %s = %.2x at %p\n", PrintVarName(c3), c3, &c3);
	printf("Variable %s = %.2x at %p\n", PrintVarName(c4), c4, &c4);
	return 0;
}

In his output


We can find that the first defined address of c1 is the highest, and the last defined address of c4 is the lowest.

This is because the address of the stack area is not in order from low to high, but the compiler first opens up the required stack memory and then puts the required variables into it.

The compiler will open up memory from low to high, and the memory opened up is stack memory.

After the development is completed, the end address is the high address, it will become the bottom of the stack, and the variables will be placed in turn, starting from the bottom of the stack and then stacked to the top of the stack.

Why choose stack as the structure to store temporary variables?

In C language, when calling a function, for the instructions at the bottom of the program, only the variables in the function scope need to be added and deleted. Then use the stack to store data. When the function is called, press the function on the stack, and reset the top of the stack when the function call is completed. This operation is extremely simple and efficient, Therefore, stack is preferred to store functions and variables within the scope.

However, one thing we have to pay attention to when operating on the stack is the way the program stores information.

When traversing a table or other data structure, there is no problem traversing from the bottom of the stack to the top of the stack under normal circumstances.

But if you want to operate the data items accurately, such as encryption or compression, you need to consider how the machine stores the data.

For example, an int occupies 4 bytes and 4 bytes occupy 4 addresses. These 4 addresses may be stored in the order of the stack itself, from low to high, or in the opposite order. This depends on whether the machine itself adopts the large end mode or the small end mode when storing data.

What is the size end?

If there is a string of memory addresses, 00 / 01 / 02 / 03 / 04 can store five values. First put the five characters of the word hello into the five values. The normal order must be 00 to store h, 01 to store e, and so on.

But one address can only store one byte, The maximum value is 255 (unsigned). If the data itself exceeds one byte, for example, 1234 takes more than one byte if it exceeds 255, then if we store 12 in 00 address and 34 in 01 address, we will get the characters in normal order. This method in line with our reading habits is called big end mode, (big endian) because he puts the number representing the large number in the large position (the value with high weight is stored first).

However, a byte has its own order, which means that the data stored in each byte is independent without weight. Instead, the data stored in a low address is the one with high weight. When traversing, we first encounter 12. When we piece it up into an integer, we have to carry out weight transformation. The final result is 12 multiplied by our own weight, that is, 100 and 34.

The addresses are sequential. If we regard the addresses as weights, increase the weight of each address increment by 100, store 12 in 01 and store 34 in 00, then it seems that we have obtained 3412, but at this time, 12 will obtain high weight according to its own high address, and the value of 1234 will be obtained directly when reading data without calculation. This logical method is the little endian mode, that is, the number representing the small is placed in the large position (the value with low weight is stored first).

In communication, it is often necessary to confirm the large and small end modes between communication machines before communication. If the large and small ends are different, the end sequence needs to be adjusted. Generally, a constant is used to judge.

How to judge the size end?

CSAPP gives the function of traversing a single byte using unsigned char * to judge the size end through character order.

//This program comes from CSAPP to test the allocation mode of the size end of the machine
#include <stdio.h>
#include <string>
typedef unsigned char* byte_pointer;
void show_bytes(byte_pointer start, int len) {
    int i;
    for (i = 0; i < len; i++)
        printf("%.2x ", start[i]);
    printf("\n");
}
int main() {
    int i = 180150000;
    i += 1;
    //This 1 can be replaced with any number less than or equal to 0xf to confirm the data difference
    printf("%x at memory array:\n", i);
    show_bytes((byte_pointer)&i, sizeof(i));
    printf("%x\n", i);
    //On different platforms, the arrangement of large and small ends is different
    //This order may be different
    const char* s = "abcdef";//There are seven in all
    printf("%s at memory array:\n", s);
    show_bytes((byte_pointer)s, strlen(s)+2); //Should print 8
    //However, the results of ascii code are almost the same on all platforms, so ascii has better platform compatibility
    return 0;
}

According to this idea, simplify the program and write a function to judge the size end

void endianMode() {
    unsigned char *cursor;          //Single byte cursor pointer
    unsigned short num = 0xff00;    //Number of bytes stored
    cursor = (unsigned char *)&num; //Implicit type conversion
    if (*cursor < *(cursor + 1))    //If the high address value is greater
        printf("little-endian");    //The machine is in small end mode
    else printf("big-endian");
}

In case of small end mode, the memory is shown as follows:

Heap area

What is a pile

The original meaning of Heap in Chinese is to pile up items. Compared with stack, its order is not required. Therefore, the original meaning of Heap in English is:

A heap of things is a pile of them, especially a pile arranged in a rather untidy way.

a rather untidy way indicates the randomness of the heap, which gives programmers greater autonomy and flexibility in allocating memory in the heap area.

Allocating heap memory through malloc is a common operation. For example, the following program manages memory through malloc and free

#include<stdio.h>
#include<malloc.h>
int main() {
	int *p = NULL;
	int *opt = NULL;
	p = (int *)malloc(sizeof(int) * 10);
	if (!p) { 
		printf("Memory allocate failed\n"); 
		return 0;
	}
	opt = p;
	for (int i = 0; i < 10; i++, opt++) 
		*opt = i;
	opt = p;
	for (int i = 0; i < 10; i++, opt++) 
		printf("%d at %p\n", *opt, opt);
	free(p);
	return 0;
}

In fact, this part is managed by the operating system and compiler, which involves the knowledge of operating system and page table, which is not expanded here.
In some cases, you need to pay attention to memory alignment when allocating heap memory.

What is memory alignment?

In the following structure, we can guess that the size of the structure is 8 bytes because two int types are used.

#include<stdio.h>
struct Example {
	int a;
	int b;
}s1;
int main() {
	printf("%d\n", sizeof(s1));
	return 0;
}

However, when we carry out network transmission and cross platform transplantation, the situation is often not so simple.

For example, the following example structure is designed.

#include<stdio.h>
struct AlignedStruct {
	int	Var_int32;
	char	Var_char1;
	char	Var_char2;
	char	Var_char3;
	char	Var_char4;
};
int main() {
	AlignedStruct s1;
	printf("%d\n", sizeof(s1));
	return 0;
}

We can also guess that this takes up 8 bytes, because an int is 4 bytes and four char s are 4 bytes.

At this time, we will adjust the order of definitions.
Withdraw

struct AligninggStruct {
	char	Var_char1;
	int	Var_int32;
	char	Var_char2;
	char	Var_char3;
	char	Var_char4;
};

The member variables of this structure do not change, but occupy 12 bytes

This is because there is no memory alignment.

Memory alignment can be visualized as the following figure:


The meaning of memory alignment is not only to save space, but also to keep the format consistent in transmission, which can reduce the occurrence of errors.

Whether it is an array, a structure, or a C + + class, due to the existence of a pointer, you can access the whole only by accessing the pointer, and then access other parts of the whole through a certain offset. Memory alignment is the embodiment of the base address + offset access method.

According to this idea, operation 123 is consistent in the following code.

#include<stdio.h>
struct AlignedStruct {
	int		Var_int32;
	char	Var_char1;
	char	Var_char2;
	char	Var_char3;
	char	Var_char4;
};
int main() {
	AlignedStruct s1, *ps1 = &s1;
	unsigned int *reg = (unsigned int *)ps1;
	(*ps1).Var_int32 = 1;		//Operation 1
	ps1->Var_int32 = 1;		//Operation 2
	*reg = 1;			//Operation 3
	printf("%p and int %p\n", ps1, &s1);
	printf("%d\n", ps1->Var_int32);
	return 0;
}

Or use the offset to operate inside the structure.

#include<stdio.h>
struct AlignedStruct {
	int		Var_int32;
	char	Var_char1;
	char	Var_char2;
	char	Var_char3;
	char	Var_char4;
};
int main() {
	AlignedStruct s1, *ps1 = &s1;
	char *reg = (char *)ps1;
	char ch = 'a';
	for (reg += 4; reg < (char *)ps1 + sizeof(s1); reg++) {
		*reg = ch++;
	}
	printf("%c\n", ps1->Var_char1);
	printf("%c\n", ps1->Var_char2);
	printf("%c\n", ps1->Var_char3);
	printf("%c\n", ps1->Var_char4);
	return 0;
}

Four characters a b c d will be output.

unsigned char is an ideal data type to represent a byte.
Of course, the above is only a concrete understanding. In the actual production environment, memory alignment is often customized by packaged tools according to requirements.
For memory operations, a large number of pointer operations are often designed. See the next article for details.

Topics: C C++ memory management