Malloc: Allocating Memory in C

Table of Contents

In compiled programming languages ​​like C, it is often useful, or even necessary, to allocate memory dynamically on the heap, in order to accommodate variables of larger or uncertain size. The malloc function allows us to ask the operating system to allocate an area of ​​memory to be used in our program.

In order to use malloc effectively, we must understand how two different parts of memory work and how our programs can make good use of them.

The Stack and the Heap

A program can use two distinct areas of RAM to store its data : the stack and the heap. The operating system manages these memory sections completely differently, which means that each of them has unique characteristics.

The stack is the default memory area our variables are assigned to when we declare them in our functions. It is streamlined and highly optimized because it operates in a sequential way and its addresses are constantly being reused. As its name suggests, data in this memory area gets piled up in order. When a function ends, the variables declared within it are automatically removed from the stack in order, from top to bottom. The stack also has a fixed size, which means we cannot store large quantities of data without risking a stack overflow.

The heap on the other hand, allows the arbitrary allocation and freeing of memory. It has no size limitations: when a process needs more memory, all it has to do is ask the operating system for it. The heap’s flexibility makes its management more complex, which in turn reduces its performance compared to the stack.

As a general rule, we will need to allocate memory to the heap in the following cases:

  • when we don’t know the size of a variable in advance.
  • if we want to be able to manipulate large amounts of data.
  • if we don’t want a variable to be removed at the end of the function in which it was declared because we need to access it from a different function.

Allocating Memory with Malloc

With the malloc (memory allocation) function, we can ask the operating system to allocate a memory area of a certain size in the heap. To use it, we must include the stdlib.h library as follows:

#include <stdlib.h>

This is the prototype of the malloc function:

void *malloc(size_t size);

Malloc returns a void pointer (which can be interpreted as any other type or data), and takes a size in bytes as a parameter.

Knowing that a char is composed of 1 byte but that it takes 4 bytes to store an int, we need to find a convenient way to determine how many bytes we need to store this or that type of data. Thankfully, we can use the keyword sizeof: for example, sizeof(char) will result in 1 byte, or 8 bits, but sizeof(int) will result in 4 bytes or 32 bits. All that’s left to do is multiply that size by the number of chars or ints that we want to allocate.

int  *i;     //Declaring an int type pointer
i = malloc(sizeof(int)); //Malloc the size of an int (4 bytes)

char *c;     //Declaring a char type pointer
c = malloc(sizeof(char)); //Malloc the size of a char (1 byte)

But, since sizeof can directly measure a variable, it would certainly be better practice (and easier to read !) to write:

int  *i;    //Declaring an int type pointer
i = malloc(sizeof *i); //Malloc size of what is at i (an int)

char *c;    //Declaring a char type pointer
c = malloc(sizeof *c); //Malloc size of what is at c (a char)

This way, if we later decide to turn our int i into a long i (8 instead of 4 bytes), we won’t have to go and modify any of our mallocs.

To malloc a string of characters, we need to multiply the number of bytes of a char by the number of chars we need in our string, without forgetting to add an extra char for the final \0.

char *cpy;
char str[7] = "Hello!";
// Let's say we want to malloc enough space to make a copy of str.
// We need to multiply the size of a char by the length of str,
// +1 for the final \0.
// [H][e][l][l][o][!][\0] = 6 char + 1 char
cpy = malloc(sizeof *cpy * (strlen(str) + 1));

Finally, to allocate an array of strings, we must first allocate the array itself, and each individual string afterwards.

char **strs;

// We malloc the array. Let's say we want 3 strings in our array:
strs = malloc(sizeof *strs * (3 + 1)); // +1 for the last \0
// Then we can malloc each string
// (in practice, we would probably do this in a loop):
strs[0] = malloc(sizeof **strs * (5 + 1)); // +1 for the final \0
strs[1] = malloc(sizeof **strs * (5 + 1));
strs[2] = malloc(sizeof **strs * (1 + 1));
strs[3] = malloc(sizeof **strs * 1);
/*
To visualize our currently empy array, let's fill it with some characters.
It will look something like this:
 [H][e][l][l][o][\0]
 [w][o][r][l][d][\0]
 [!][\0]
 [\0]
*/

Best Practices in Terms of Malloc

To Cast or Not to Cast a Malloc?

It is not necessary and not even advisable to cast a malloc. Since the function returns a void type pointer, it is automatically converted to the correct type during the assignation. Not only does a cast clutter up the code for no reason, it could even have the undesirable effect of masking potential errors later on…

int *i;

i = (int *)malloc(sizeof(int) * 1); // Ugly and risky
i = malloc(sizeof(int) * 1);  // Better
i = malloc(sizeof *i * 1);   // Pretty and secure

The last option is not only easier to read, it also automatically takes into account any change in variable type.

Safeguarding a Malloc

On the other hand, it is strongly advised to systematically add a safeguard after calling the malloc function, just in case the operating system fails to allocate the memory we asked for. Two more lines should do the trick:

int *x;

x = malloc(sizeof *x * 10);
if (x == NULL)
  return ;  //or return (-1); or return (NULL);

Freeing Memory with Free

Every malloc has its corresponding free. Freeing memory from the heap is the programmer’s responsibility.

Of course, modern operating systems automatically free any used memory when a program ends, but it is good practice nonetheless. If a program must run 24/7, on a server, for instance, freeing the memory becomes vital. The same goes for programs that are required to function in environments with fewer resources. And for the students of 42, it is mandatory.

#include <stdlib.h>

int main(void)
{
  char *ptr;

  ptr = malloc(sizeof *ptr * 25 + 1);
  if (!ptr)
    return (-1);

  free(ptr);
  return (0);
}

Of course, to free an array of strings, we must free each individual allocated string before freeing the array itself.

Any heap-allocated memory must be freed.

Golden rule of C programming.

There are several ways to check for memory leaks in a program. Under MacOS, we can compile the program with the -fsanitize=address option. If there is a leak, it will be clear when we execute the program. However, the best tool to detect and debug a memory leak is Valgrind under Linux, which provides more useful information.

Let’s Code a Simple Malloc

In this exercise we will allocate memory for an array of integers with the malloc function. Then, we will fill the array with numbers from 0 to 9, and print it out. And finally, we won’t forget to free the memory at the end. (All that if, and only if, the memory allocation doesn’t fail!)

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
  int *tab;
  int i;

  tab = malloc(sizeof *tab * 10);
  if (tab == NULL) // If allocation failed
    return (-1); // Stop

  i = 0;
  while (i < 10) // Fill array with numbers
  {
    tab[i] = i;
    i++;
  }

  i = 0;
  while (i < 10) // Print array
  {
    printf("%d ", tab[i]);
    i++;
  }

  printf("\n");
  free(tab);
  return (0);
}

Sources and Further Reading

  • Linux Programmer’s Manual, malloc(3) [link]
  • Zeste de Savoir, Le langage C : L’allocation dynamique [link]
  • Buzut, RAM, Stack, Heap : les différents types de mémoires informatiques [link]
  • Stack Overflow, Do I cast the result of malloc? [link]
  • Stack Overflow, What REALLY happens when you don’t free after malloc? [link]
  • Wikipedia, sizeof [link]

Comments

Related Posts

Threads, Mutexes and Concurrent Programming in C

For efficiency or by necessity, a program can be concurrent rather than sequential.

Read More

Pipe: an Inter-Process Communication Method

By default, it is difficult to get two processes to communicate with each other.

Read More

Errno and Error Management in C

Underlying any software development is program error detection and analysis. But then an external library function or system call fails, how can we understand what went wrong?

Read More