Dynamic memory allocation

Location
  1. Courses

    /

  2. Complete C++ Course

    /

  3. The C Standard Library

    /

  4. Dynamic memory allocation

Static vs dynamic memory

Until then, we only used static memory. That memory is automatically managed by the compiler. For example, when we define a variable, the compiler automatically gives it a memory location for the duration of its life span. The static memory is the memory space where the program is stored when it is loaded to the memory. Dynamic memory allocation allows us to allocate, manually, any amount of memory (That fits in the RAM) (Located outside of the static memory) at any moment and free it when we do not need it anymore. Allocating memory basically means reserving a memory space. A memory location that has been allocated inside braces {} still exist at the end of them, since the compiler does not free it automatically. We have to do it manually when we do not need it anymore. The C and C++ standard libraries both provide their tools to dynamically allocate memory, but we will only see the C way to do it here, because we must understand classes to really understand the C++ way.

Allocating memory dynamically

The header of the function, from the C Standard Library, to allocate memory is void* malloc(size_t size). It takes, in argument, the amount of bytes we want to allocate, then allocate the memory space and returns a pointer of type void to the start of it. Note that when we allocate memory, we actually allocate an array of data. What is the type of data? We decide. In the end, variables are simply bytes stored in the memory. For example, if we cast the void pointer to an int pointer, then the array will be considered to be an array of int. The type of the data stored in the memory is what we consider it to be (How we interpret it). For example, he bits 1101 1101b has a different meaning (value) whether we consider them to represent a value of type unsigned char or char.

Like the other functions of the C Standard Library that handle dynamic memory allocation, malloc is declared in the header <stdlib.h>.

Note that malloc takes, as argument, the amount of bytes we want to allocate, not the amount of elements. So, if we want to allocate 4 variables of type int, we must give it 4*sizeof(int) as argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdlib.h> #include <iostream> int main() { /* We tell malloc to allocate (8*sizeof(double)) bytes and then we tell the compiler to interpret the returned void pointer as a pointer of type double (By type-casting it). Because of the amount of bytes we allocated, ptr points to an array big enough to contain 8 elements of type double. */ double *ptr = (double*)malloc(8 * sizeof(double)); /* We assign a value to the first element (Interpred as a variable of type double) from the memory space allocated. */ ptr[0] = 932.54; /* We print the value of the first element (Interpred as a variable of type double) inside the memory space allocated. */ std::cout << ptr[0] << std::endl; return 0; }

If malloc fails to allocate the memory (Example: The amount of bytes wanted is too big), it will return a null pointer (The address 0).

Here is an example where we verify that the memory allocation had been successful:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h> #include <iostream> int main() { char *ptr = (char*)malloc(8); // Allocates 8 bytes (char). if(ptr) // If ptr != 0 (false). std::cout << "Memory allocation successful!" << std::endl; else std::cout << "Memory allocation unsuccessful." << std::endl; return 0; }

Note that we should not give 0 as an argument to malloc. If we do, malloc might return a null pointer, but it is not guaranteed.

Free the allocated memory space

When we allocate memory dynamically, it is our job to free it. As long as we do not free it (Or the program ends), the memory space we allocated stays reserved and therefore is not free to be used for other things. All we have to do to free a memory space allocated using malloc (Or calloc), is calling the function free and gives it, as argument, a pointer to it. The header of the function free is void free(void* ptr).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdlib.h> #include <iostream> int main() { char *ptr = (char*)malloc(8); // We allocate 8 char (bytes). // We use the dynamically allocated memory space. ptr[4] = 'S'; std::cout << ptr[4] << std::endl; free(ptr); // We free the 8 char (bytes) we allocated. return 0; }

Resize a dynamically allocated memory space

Let us say we allocated 60 char (byte) and then, later, we need that array to be 80 char (byte) long. We could dynamically allocate an other array of 80 bytes, copy the data from the first array to the second one and then free the first array.

However, the best way to do that is to use the function realloc. Its header is void* realloc(void *ptr, size_t size). The first argument must be a pointer to the dynamically allocated memory space we want to resize and the second one must be the new size we want it to have. That function tries to resize the memory space given in argument, which has been allocated with malloc (Or calloc), and if it fails, it does what explained before: Allocate a new memory space, copy the data of the first array to it and then free the old memory space. If it succeeds in resizing the dynamically allocated memory space given in argument, it returns the pointer back, otherwise, it returns a pointer to the newly allocated memory space. If realloc fails to reserve a memory space of the size given in second argument, the memory space given in argument stays intact and a null pointer is returned.

Here is a simple example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdlib.h> #include <iostream> int main() { char *ptr = (char*)malloc(8); // We allocate 8 bytes. ptr[3] = 'A'; std::cout << "Index 3: " << ptr[3] << std::endl; ptr = (char*)realloc(ptr, 16); // We change the size of the memory space to 16 bytes. std::cout << "Index 3: " << ptr[3] << std::endl; ptr[12] = 'B'; std::cout << "Index 12: " << ptr[12] << std::endl; free(ptr); // We free the 16 char we allocated. return 0; }

In the example above, we assumed the function realloc operated successfully. Here is a more complex example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdlib.h> #include <iostream> int main() { char *ptr = (char*)malloc(8); // We allocate 8 bytes. ptr[3] = 'A'; std::cout << "Index 3: " << ptr[3] << std::endl; char *newPtr = (char*)realloc(ptr, 16); // We change the size of the memory space to 16 bytes. if(newPtr == 0) // If an error happened. { std::cout << "realloc failed to create a memory space of the size requested.\n"; free(ptr); // We free the 8 char we allocated. return 0; // We quit the program. } else if(newPtr == ptr) // If the memory space had been resized. std::cout << "realloc successfully resized the memory space.\n"; else // If a new memory space had been allocated. { std::cout << "realloc had to allocate a new memory space.\n"; ptr = newPtr; } std::cout << "Index 3: " << ptr[3] << std::endl; ptr[12] = 'B'; std::cout << "Index 12: " << ptr[12] << std::endl; free(ptr); // We free the 16 char we allocated. return 0; }

In the following example, the size of the memory space is reduced:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdlib.h> #include <iostream> int main() { char *ptr = (char*)malloc(16); // We allocate 16 bytes. char *newPtr = (char*)realloc(ptr, 8); // We change the size of the memory space to 16 bytes. if(newPtr == 0) // If an error happened. { std::cout << "realloc failed to create a memory space of the size requested.\n"; free(ptr); // We free the 16 char we allocated. return 0; // We quit the program. } else if(newPtr == ptr) // If the memory space had been resized. std::cout << "realloc successfully resized the memory space.\n"; else // If a new memory space had been allocated. { std::cout << "realloc had to allocate a new memory space.\n"; ptr = newPtr; } free(ptr); // We free the 8 char we allocated. return 0; }

Allocating memory dynamically using calloc

The function which header is void* calloc(size_t count, size_t size) is similar to malloc, except that it takes two arguments and that it sets all the allocated bytes to the value 0. The first argument is the number of elements to allocate and the second one is the size, in bytes, of one element.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdlib.h> #include <iostream> int main() { int *ptr; ptr = (int*)calloc(8, sizeof(int)); // We allocate 8 int that are set to 0. // Prints the first two int. std::cout << ptr[0] << ' ' << ptr[1] << std::endl; free(ptr); // We free the 8 int we allocated. return 0; }

Note on static arrays

The size of static arrays does not have to be known at compilation time. The following example works:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream> int main() { std::cout << "Enter the size of the array: " << std::endl; unsigned size; std::cin >> size; int array[size]; return 0; }

However, static arrays have two problems. First, their size can not change once they are defined. Secondly, the size of the static memory is limited. If we try to define an array with a size that is too big (That does not fit in the static memory), the program will crash. Defining a static array of only 4 megabytes (4000 000 bytes) can be enough to make the program crash.

In the picture above, the error code different than 0 indicates that the program crashed.

Note on the C++ way to manage dynamically allocated memory

Even tough the C++ way to a manage dynamically allocated memory does additional things when it comes to class objects (variables of class types), it does not provide any way to resize a memory space. We must, manually, allocate a new memory space, copy the data from the first array to it and then free the old memory space.