© Mikael Olsson 2019
Mikael OlssonModern C Quick Syntax Referencehttps://doi.org/10.1007/978-1-4842-4288-9_19

19. Memory Management

Mikael Olsson1 
(1)
Hammarland, Länsi-Suomi, Finland
 

In the examples so far, the programs have only had as much memory available as has been declared for the variables at compile time. This is referred to as static allocation. If any additional memory is needed at runtime, it becomes necessary to use dynamic allocation. The C standard library provides several functions for managing dynamically allocated memory, including malloc, free, realloc, and calloc. These functions are found in the stdlib.h header file.

Malloc

The malloc function takes a size in bytes and returns a pointer to a block of free memory of that size. This dynamically allocated memory is uninitialized and can only be accessed through pointers.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
  /* Dynamic memory allocation (1*5 = 5 bytes) */
  char* ptr = malloc(sizeof(char) * 5);
}
The sizeof operator is used to get the number of bytes for the given data type on the current system. This number is multiplied by five to allocate a block large enough to contain exactly five chars, provided that the system has that much free memory available. If it does not, malloc returns null to signal that it has failed to allocate the memory, in which case the function may need to signal to its caller that it too has failed.
char* ptr = malloc(sizeof(char) * 5);
if (ptr == NULL) {
  /* No memory allocated, exit function */
  return -1;
}

By convention, functions that return a pointer, such as malloc, use the value NULL to indicate failure. This is different from functions that return a value, which traditionally use 0 to indicate success and -1 to signal failure. Additional return values can be used to give the user of the function more detailed information about the return state.

Free

An important thing to remember about dynamic allocation is that this memory will not be released when the pointer goes out of scope, as with local variables. Instead, the memory has to be manually released with the function free , which releases the memory block at the specified address.
free(ptr); /* release allocated memory */

This allows you to control the lifetime of a dynamically allocated object, but it also means that you are responsible for freeing that memory once it is no longer needed. Forgetting to free dynamic memory will give the program unwanted memory leaks, because that memory will stay allocated until the program shuts down.

A pointer to released memory should be set to NULL immediately to show that it is no longer set to a valid reference. This is especially important for pointers that are repeatedly allocated and freed within a program, as trying to free an already freed memory block leads to undefined behavior. Likewise, a NULL check should be carried out prior to using a pointer to make sure it is valid.
if (ptr) { /* same as: if (ptr != NULL) */
  free(ptr);
  ptr = NULL; /* null pointer */
}

It is interesting to note that the function free only accepts one argument, the starting address for the memory block. The actual size of the block does not need to be provided. This is because the implementation of malloc and free keeps track of the size of each block as it is allocated, typically by storing the size next to the block.

Realloc

An allocated memory block can be resized with the realloc function . This function takes two arguments: the pointer to a previously allocated memory block and the new total size requested. If the pointer passed to realloc is NULL then the function behaves as malloc.
/* Allocate space for 5 chars */
char* p = malloc(sizeof(char) * 5);
/* Increase size to 10 chars */
char* new_p = realloc(p, sizeof(char) * 10);
This return value is stored in a new pointer, in case realloc fails to allocate the extra memory and returns NULL. This prevents the only reference to the previously allocated memory block from being lost, which would lead to a memory leak.
/* On failure, free memory and exit */
if (!new_p) { /* if (new_p == NULL) */
  if (p) {
    free(p);
    p = NULL;
  }
  return -1;
}
/* On success, update pointer */
else {
  p = new_p;
}

Calloc

The memory block returned by malloc is uninitialized, so it will contain whatever data happens to be in that memory region. If it is important to fill the memory block with zeroes then the calloc function can be used instead. In addition to initializing the memory to zero, this function takes an extra argument in the first position, specifying the number of blocks to be allocated.
int *a;
/* Allocate memory for 3 integers, all set to 0 */
a = (int*)calloc(3, sizeof(int));
printf("Sum is: %d\n", a[0]+a[1]+a[2]); /* "Sum is 0" */
free(a); /* deallocate memory */
Another way to initialize a memory block is with the memset function. This function takes three arguments: a pointer to the starting address, the value to be filled, and the number of bytes to fill with this value.
int *arr;
int size = 3 * sizeof(int);
/* Allocate memory for 3 integers */
arr = (int*)malloc(size);
/* Fill entire memory block with zeros */
memset(arr, 0, size);
free(arr); /* deallocate memory */

Void Pointers

It is sometimes necessary to use pointers without regard to the type they reference. This is achieved by specifying the pointer type as void*, known as a void pointer. A void pointer can store the address of any type of variable and can be cast to any pointer type, making them useful as a universal pointer. This type is what allows the free function to accept any pointer argument, and allows malloc to return a pointer that can be cast to any pointer type. The following example illustrates how the void pointer can be used to change the values of two variables of different types.
int i;
char c;
/* Change i through void pointer */
void *vptr = &i;
*((int*)vptr) = 1;
/* Change c through void pointer */
vptr = &c;
*((char*)vptr) = 'a';

Note that a void pointer may not be dereferenced without first casting it to the appropriate pointer type. The compiler is unable to check that this type cast is valid, which is why void pointers should be used with care.

Function Pointers

Another useful application of pointers is to point to functions. This allows functions to be passed to other functions or stored in arrays. Consider the following function.
void func(int x) {
  printf("Value is %d\n", x);
}
A function pointer that can reference this function needs to have a matching return type and list of argument types. Note the parentheses wrapped around the pointer identifier in the following function pointer declaration. They are important so that the compiler does not view this as a function declaration.
void (*p)(int);
Now a reference to the function can be assigned to this function pointer. Since the compiler knows this is a function the address-of operator (&) is not necessary. Also, since this pointer points to code and not data, this memory must not be deallocated with free.
p = func;
p = &func; /* alternative */
The function can be called like an ordinary function. The compiler will automatically dereference the function pointer if necessary when it is called as a function.
func(5); /* "Value is 5" */
(*func)(5); /* alternative */
Function pointers can be passed to other functions, allowing functionality to be plugged into existing code. Keep in mind that the return type and list of argument types of the passed function pointer need to match the parameter of the function.
void mycaller(void (*a)()) {
  a();
}
void myfunc() {
  printf("Hello World\n");
}
int main(void) {
  mycaller(&myfunc); /* & is optional */
}