Skip to content Skip to main navigation Skip to footer

C Memory Management

Memory management in C is divided into two parts. One part is managed by the system, and the other part is managed by the user.

The memory managed by the system is mainly the variables inside the function (local variables). These variables go into memory when the function is running and are automatically unloaded from memory at the end of the function. The area where these variables are stored is called the “stack“, and the memory where the “stack” is located is managed automatically by the system.

The memory that is managed manually by the user is mainly those variables that are present throughout the program runtime (global variables) and need to be manually released from memory by the user. If you forget to release a variable after using it, it will continue to occupy memory until the program exits, a situation known as a “memory leak“. The memory where these variables are located is called the “heap” and the memory where the “heap” is located is managed manually by the user.

Void pointer

As described in the previous posts, each memory block has an address, and the memory block at the specified address can be accessed by a pointer variable. The pointer variable must have a type, otherwise the compiler would not know, how to interpret the binary data stored in the memory block. However, when requesting memory from the system, it is sometimes not certain what kind of data will be written to memory, so it is necessary to allocate the memory block first and then determine the type of data to be written.

 The C programming language provides an indeterminate type of pointer, called a void pointer. It has only the address information of the memory block, without type information, and waits until the memory block is used to add a note to the compiler about what type of data is inside.

On the other hand, a void pointer is equivalent to an untyped pointer that can point to any type of data, but cannot interpret the data. void pointers are interconvertible with all other types of pointers; any type of pointer can be converted to a void pointer, and a void pointer can be converted to any type of pointer.

int x = 10;
void* p = &x; // Converting integer pointers to void pointers
int* q = p; // void pointer converted to integer pointer

&x is an integer pointer, p is a void pointer, and the address of &x is automatically interpreted as a void type when it is assigned. Similarly, when p is then assigned to an integer pointer q, the address of p is automatically interpreted as an integer pointer.

Note: since it is not known what type of value the void pointer points to, you cannot use the * operator to take out the value it points to.

char a = 'X';
void* p = &a;
printf("%c\n", *p); // error

p is a void pointer, so you cannot use *p to retrieve the value pointed to by the pointer.

malloc()

The malloc() function is used to allocate memory. It requests a block of memory from the system, and the system allocates a contiguous block of memory for it in the “heap“. Its prototype is defined in the header file stdlib.h.

 void* malloc(size_t size)

It accepts as an argument a non-negative integer indicating the number of bytes of memory to be allocated, and returns a void pointer to the allocated block of memory. This makes sense because the malloc() function does not know what type of data will be stored in that block of memory, so it can only return an untyped void pointer.

You can use malloc() to allocate memory for any type of data. Typically, the sizeof() function is first used to calculate the length of bytes needed for a certain data type, and then that length is passed to malloc().

int* p = malloc(sizeof(int));
*p = 12;
printf("%d\n", *p); // 12

 malloc() may fail to allocate memory and return the constant NULL, which has a value of 0 and is a memory address that cannot be read or written, and can be interpreted as a pointer to nowhere. It is defined inside several header files, including stdlib.h, so NULL can be used whenever malloc() can be used. because of the possibility of allocation failure, it is best to check for successful allocation after using malloc().

int* p = malloc(sizeof(int));
 
if (p == NULL) {
  // Memory allocation failed
}

// or
if (!p) {
  //...
}

Whether malloc() allocates successfully or not is determined by whether the returned pointer p is NULL.

free()

free() is used to release the memory allocated by the malloc() function by returning the block to the system for reuse, otherwise the block will be occupied until the end of the program. The prototype of this function is defined in the header file stdlib.h.

void free(void* block)

 The argument to free() is the address of the memory returned by malloc().  

int* p = (int*) malloc(sizeof(int));  
*p = 12; 
free(p);

Note: once an allocated block of memory is freed, you should not manipulate the freed address again, also you should not use free() again to free the address a second time.

A very common mistake is to allocate memory in a function, but not use free() to free it at the end of the function call.

void test(double arr[], int n) {
  double* temp = (double*) malloc(n * sizeof(double));
  // ...
}

 The function test() allocates memory internally, but does not write free(temp). This causes the occupied memory block to remain after the function ends, and if test() is called multiple times, multiple memory blocks will be left behind.

calloc()

The calloc() function works similarly to malloc() in that it also allocates blocks of memory. The prototype of this function is defined in the header file stdlib.h.

The difference between these two functions is as follows:

  • calloc() accepts two arguments, the first one is the number of values of a certain data type and the second one is the unit byte length of that data type.
void* calloc(size_t n, size_t size);
  •   calloc() initializes all allocated memory to 0. malloc() does not initialize memory, and if you want to initialize to 0, you have to call the memset() function additionally.
int* p = calloc(10, sizeof(int));
// Equivalent to
int* p = malloc(sizeof(int) * 10);
memset(p, 0, sizeof(int) * 10);

realloc()

The realloc() function is used to modify the size of an allocated memory block, either by scaling it up or down, returning a pointer to the new memory block. The prototype of this function is defined in the header file stdlib.h.

 void* realloc(void* block, size_t size)

It accepts two arguments.

  • block: a pointer to an already allocated block of memory (generated by malloc() or calloc() or realloc()).
  • size: the new size of this memory block, in bytes.

realloc() can return a new address (the data is also automatically copied) or the same address as the original. realloc() gives priority to reducing the original memory block as much as possible without moving the data, so it usually returns the original address. If the new memory block is smaller than the original size, the excess is discarded; if it is larger than the original size, the new part is not initialized (the programmer can automatically call memset()).

Here is an example where b is an array pointer and realloc() dynamically resizes it.

int* b;
b = malloc(sizeof(int) * 10);
b = realloc(b, sizeof(int) * 2000);

Pointer b initially points to a 10-member array of integers, which is resized to a 2000-member array using realloc(). This is the benefit of manually allocating the array memory, allowing the length of the array to be adjusted at runtime at any time.

restrict keyword (type qualifier)

When declaring a pointer variable, you can use the restrict type qualifier to tell the compiler that the block of memory can only be accessed by the current pointer and that no other pointers can read or write to that block of memory. This type of pointer is called a “restricted pointer“.

int* restrict p;
p = malloc(sizeof(int));

The above code segment declares the pointer variable p with restrict type qualifier, which makes p as a restricted pointer. When the pointer p points to a block of memory returned by the malloc() function, that block can only be accessed by the pointer p.

memcpy()

memcpy() is used to copy a block of memory to another block of memory. The prototype of this function is defined in the header file string.h.

void* memcpy(

  void* restrict dest, 

  void* restrict source, 

  size_t n

);

In the above code segment, the parameter dest is the destination address, the parameter source is the source address, and the third parameter n is the number of bytes n to be copied. n is equal to 10 * sizeof(double) if 10 array members of type double are to be copied.

Both dest and source are void pointers, indicating that there is no restriction on the type of pointer here and that all types of memory data can be copied. Both have the restrict keyword, indicating that these two memory blocks should not have areas that overlap each other.

memmove()

The memmove() function is used to copy a section of memory data to another section of memory. Its main difference from memcpy() is that it allows the target area to have overlap with the source area. If there is an overlap, the contents of the source region will be changed; if there is no overlap, its behavior is the same as memcpy().

The prototype of this function is defined in the header file string.h.

void* memmove(
  void* dest, 
  void* source, 
  size_t n
);

 The return value of memmove() is the first argument, which is a pointer to the target address.

int a[100];

// ...

memmove(&a[0], &a[1], 99 * sizeof(int));

Starting from member a[1], each of the 99 members of the array is moved forward one position.

memcmp()

The memcmp() function is used to compare two memory regions. Its prototype is defined in string.h.

int memcmp(

  const void* s1,

  const void* s2,

  size_t n

);

It accepts three parameters, the first two of which are pointers to the comparison, and the third specifies the number of bytes to be compared.

Its return value is an integer. Each byte of the two memory regions is interpreted as a character form and compared in dictionary order, returning 0 if they are the same, an integer greater than 0 if s1 is greater than s2, and an integer less than 0 if s1 is less than s2.

char* s1 = "abc";

char* s2 = "acd";

int r = memcmp(s1, s2, 3); // less than 0

Was This Article Helpful?

2
Related Articles
0 Comments

There are no comments yet

Leave a comment

Your email address will not be published.