DEV Community

mikkel250
mikkel250

Posted on • Edited on

Dynamic memory allocation in C

Overview

This post deals with pointers because dynamic memory allocation only works with pointers.
Whenever you define a variable in C, the compiler automatically allocates the correct amount of memory based on the data type that is declared.
However, it is frequently desireable to be able to dynamically allocate storage while a program is running, e.g. if you want to change the value of, or concatenate to a string, or if you have a program that is designed to read in a set of data from a file into an array in memory. To allocate memory in C, there are three options:

  1. define the array to contain the maximum possible number of elements at compile time
  2. use a variable-length array to dimension the size of the array at runtime (available in the C99 standard)
  3. allocate the array dynamically using the memory allocation routines availble in the C language

Allocating memory dynamically allows you to create pointers at runtime that are just large enough to hold the amount of data required for the task, and free it when no longer needed - ideal from an efficiency perspective.

Heap vs. Stack

There are two ways to store memory in a C program (two data structures): in the heap, or in the stack. These will not be covered in depth, but the heap is where dynamically allocated memory reserves space. Memory in the heap persists longer than memory on the stack, and the programmer controls when it is freed.

Things such as function arguments and local variables are stored in the stack, and the memory is automatically freed when a function ends.

The advantage of allocating memory, as with everything in programming, is a trade-off: you as the programmer must also keep track of the memory that has been allocated and remember to free it when it is no longer needed.

Memory allocation in C

There are three functions in the standard library that allow you as the programmer to work with memory: malloc(), calloc(), and realloc(). To use them, include the standard library at the top of your file: #include <stdlib.h>

Memory allocation: malloc()

malloc() is the simplest standard library function that allocates memory at runtime. The number of bytes to be allocated is the argument, and it returns the address of the first byte of memory that it allocated. Because the return value is a memory address, it is declared as a variable to store the return value, which can only be a pointer (because only pointers can store memory addresses). The simplest way it can be declared is shown in the example below:

int * pNumber = (int*) malloc(100);
Enter fullscreen mode Exit fullscreen mode

In the above, 100 bytes of memory were requested, and the address is assigned to the pointer named pNumber, which points to the first byte in that location. Assuming an int takes up 4 bytes, it would hold 25 int values. However, this is not the ideal way to allocate memory, because different systems will have different sizes for integers.
The better way to use malloc() is to take the desired number of values to be stored, call the sizeof() operator to get the size of the data type, and then multiply the desired number of values by the value returned by sizeof()

int * pNumber = (int*) malloc(25*sizeof(int));
Enter fullscreen mode Exit fullscreen mode

Using the method above ensures that the exact amount of memory required to hold 25 integers is allocated, regardless of system (32 or 64 bit PC, or imbedded systems with different architecture like a raspberry pi or IOT device). Because malloc returns a void type pointer, you must always cast the return value to the correct data type when allocating memory - the (int*) in this case.
You can request any number of bytes, but if for some reason the amount requested cannot be allocated (e.g. there is not enough memory to do so for a huge amount requested), then malloc() returns a pointer with the value NULL. For this reason, as part the declaration, it is always a good idea to also include an immediate validation check and code to deal with a failure to allocate, so the complete code for malloc() would look like this:

int * pNumber = (int*) malloc(25*sizeof(int));
if(!pNumber)
{
  // code to deal with the failure, such as an error message and program exit/abort
}
Enter fullscreen mode Exit fullscreen mode

Remember, a NULL value is falsy, so the if loop will only run if pNumber is NULL or zero (boolean false in C). If the memory can't be allocated, it's best to throw an error and exit/abort the program, because it will crash later (e.g. when the program later tries to use the array) and it will be harder to determine why when debugging later.

Releasing memory

The memory will automatically be released when the program ends, but when you no longer need the memory, it is best practice to always release it, even if it's just before the program exits to avoid memory leaks.
A memory leak can occur when you allocate memory and do not retain a reference to it, and thus are unable to release it. This often occurs in a loop, and because it is not released when no longer required, the program will consume more and more on each loop iteration and eventually may occupy it all. Ideally, the function that allocates memory will be the one that releases it.
To release the memory, the standard library provides a free() function to do so. The free() function has a formal parameter of type void*, which means any type of pointer can be passed to it as an argument. As long as the pointer contains the original address that was returned when the memory was allocated, the entire block of memory will be freed.
The pointer should always be set to NULL after the memory has been freed, the syntax of which is demonstrated below:

free(pNumber);
pNumber = NULL;
Enter fullscreen mode Exit fullscreen mode
Continuous allocation: calloc()

The calloc() function is similar to malloc(), the difference being that it initializes the memory after it is allocated so that all bytes are zero. It takes two arguments, the number of data items required, and the size of each data item. So, the declaration looks a lot like malloc(), except the number of items is its own argument, and you don't need to manually multiply it by the size:

int * pNumber = (int*) calloc(75, sizeof(int));
Enter fullscreen mode Exit fullscreen mode

The return value will be NULL if it is not possible to allocate the memory requested.

Reallocating memory: realloc()

The realloc() function could be described in simple terms as 'copy and make bigger.' It enables you to reuse or extend memory that you previously allocated using malloc() or calloc(). It expects two arguments: a pointer containing an address that was returned by a previous malloc() or calloc() call, and the size in bytes of the new memory required.
What it will do 'under the hood' is to transfer the contents of the previously allocated memory referenced by the pointer that is passed in to a new (larger) location. The important feature of realloc() is that it preserves the contents of the original memory area.
It returns a void* type pointer to the new memory if successful, or NULL if the operation fails.
An example of the syntax:

#include <stdio.h>

int main()
{
  char * str = NULL;

  //initial memory allocation
  str = (char *) malloc(15 * sizeof(char));
  strcpy(str, "mikkel");
  printf("String: %s, Address: %p \n", str, str);

  //reallocating the memory to make it larger
  str = (char *) realloc(str, 25 * sizeof(char));
  //check the string and memory address after reallocation
  printf("String: %s, Address: %p \n", str, str);

  free(str);

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Some caveats:

  • Avoid allocating lots of small blocks of memory. Because allocating memory on the heap carries some overhead, allocating many small blocks will carry much more computational overhead than allocating fewer larger blocks.

  • Only hang on to memory as long as you need it. Free it as soon as you are finished with it.

  • Always ensure that you provide for releasing memory that was allocated - a good guideline is to decide where in the code to release it when you write the code that allocates it.

  • Make sure not to inadvertently overwrite the memory address that was allocated in the heap before it is released. This will cause a memory leak. A related warning is to be careful when allocating memory within loops.

Top comments (11)

Collapse
 
step_prosperi profile image
Stefano Prosperi

Casting the result of malloc is unnecessary and very bad practice.

Bad:

int * pNumber = (int*) malloc(25*sizeof(int));

Good:

int * pNumber = malloc(25*sizeof(int));
Collapse
 
therselman profile image
therselman • Edited

I think you are wrong about casting malloc is bad practice. You will get a compiler warning if you don't! It's total rubbish that it's bad practice! If you are not getting compiler warnings, then you aren't using a high enough warning level! It's more an inconvenience than anything else, that's why C++ created the Auto keyword, but even then you would need to cast it so that auto can infer the correct type.

Collapse
 
step_prosperi profile image
Stefano Prosperi • Edited

stackoverflow.com/a/605858/9815377

It's important to stress that we're talking about C, not C++

Thread Thread
 
ac000 profile image
Andrew Clayton

Indeed.

I think it is also worth explicitly stating that these days C and C++ are very different languages. (All too often I see people conflating the two or just saying C/C++, when they really mean one or the other...)

Collapse
 
eccles profile image
Paul Hewlett

The realloc example, whilst correct, is bad practice. What if realloc fails? In that case you will lose reference to the previously allocated str. The original memory will be orphaned.

Collapse
 
mikkel250 profile image
mikkel250

Can you elaborate and/or provide an example of what is considered best practice?

Collapse
 
eccles profile image
Paul Hewlett

Use a dummy ptr something like this...

Void *ptr = realloc(str, new size)
If (!ptr) {
.... Handle error...
}
str = ptr

This way if realloc fails the reference to str is not lost.

Of course in a normal Linux environment it is rare for any of the allocation functions such as malloc, calloc etc.. to fail. See anything about overcommit for an explanation.. This would make good subject for a followup article.

Thread Thread
 
mikkel250 profile image
mikkel250 • Edited

Thanks! These are my notes as I learn the language, so comments and clarifications like this are most welcome 😀👍

Thread Thread
 
eccles profile image
Paul Hewlett

Pleasure.

You might be interested in how linux actually allocates memory and why the malloc() type functions actually rarely fail.

Google overcommit, oomkiller and page tables.

Might make a good subject for your next blog

Collapse
 
tomlankhorst profile image
Tom Lankhorst

Things such as function arguments and local variables are stored in the heap, and the memory is automatically freed when a function ends.

You probably meant on the stack here.

Collapse
 
mikkel250 profile image
mikkel250

I did, thanks for the catch!