Improve C dynamic memory guide (#23849)

- Fix many errors and facts to be more accurate.
- Add more examples and cover all four commonly used functions.
- Rewrite parts to be more clear.
- Add some useful notes and warnings ... memory allocation is error prone!
This commit is contained in:
Javed Mohamed
2019-02-23 23:18:22 -06:00
committed by Randell Dawson
parent 8128ab00ea
commit 01b8030bbe

View File

@ -1,34 +1,45 @@
---
title: Dynamic Memory Management
---
# Dynamic Memory Management
Sometimes you will need to allocate memory spaces in the heap also known as the dynamic memory. This is particulary useful when you do not know during compile time how large a data structure (like an array) will be.
## An Example
Here's a simple example where we allocate an array asking the user to choose the dimension
Here's a simple example where we allocate an array, asking the user to choose the size.
```C
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int arrayDimension,i;
int* arrayPointer;
printf("Please insert the array dimension:");
scanf("%d",&arrayDimension);
arrayPointer = (int*)malloc(sizeof(int)*arrayDimension);
if(arrayPointer == NULL){
printf("Error allocating memory!");
return -1;
}
for(i=0;i<arrayDimension;i++){
printf("Insert the %d value of the array:",i+1);
scanf("%d\n",arrayPointer[i]);
}
free(arrayPointer);
return 0;
int dim, i;
int *array;
// Get user-defined size of array
printf("Please insert the array dimension: ");
scanf("%d", &dim);
array = malloc(sizeof(int) * dim);
// Check if there was a failure in allocating memory
if (array == NULL) {
printf("Error allocating memory!");
return -1;
}
// Get array elements
for (i = 0; i < dim; i++) {
printf("Insert the %d value of the array: ", i);
scanf("%d", array + i);
}
// Print out contents of array
for (i = 0; i < dim; i++)
printf("%d%s", array[i], (i == dim - 1) ? "\n" : ", ");
// Cleanup
free(array);
return 0;
}
```
@ -41,58 +52,107 @@ Let's see how to use this function step by step:
```C
sizeof(int)
```
Let's start from `sizeof`. The `malloc` needs to know how much space allocate for your data. In fact a `int` variable will use less storage space then a `double` one.
It is generally not safe to assume the size of any datatype. For example, even though most implementations of C and C++ on 32-bit systems define type int to be four octets, this size may change when code is ported to a different system, breaking the code.
`sizeof` as its name suggests generates the size of a variable or datatype.
Let's start from `sizeof`, a sweet C macro. `malloc` only cares about memory, and thus allocates the number of bytes you ask it. Not the number of `int`s or `float`s, **the number of bytes**. But how do you _know_ how many bytes a given type needs?
Enter the `sizeof` macro.
**Note:** Never assume the size a type needs since they can change based on implementation and system.
```C
arrayPointer = (int*) malloc(sizeof(int) * arrayDimension);
array = malloc(sizeof(int) * dim);
```
In this example, malloc allocates memory and returns a pointer to the memory block. The size of the block allocated is equal to the number of bytes for a single object of type int multiplied by `arrayDimension`, providing the system has enough space available.
But what if you do not have enough space or `malloc` can not allocate it for some other reasons?
## Checking the malloc output
This do not happens commonly but it is a very good practice to check the value of your pointer variable after allocating a new space of memory.
Here, malloc takes the number of bytes we want (the size of an `int` multiplied by how many of them we want) and gives us a pointer to a memory block with at least that much space ... most of the time. If we run out of memory (or some other error occurs), `malloc` returns `NULL`!
### Checking the malloc output
Running out of memory, while usually not a problem for small programs, is a potential source for segfaults and other bugs. Thus, we should always check to see if `malloc` succeeded or not before we try to use the pointer.
```C
if(arrayPointer == NULL)
printf("Error allocating memory!");
if (array == NULL) {
printf("Error allocating memory!");
return -1;
}
```
This will also be very usefull during your debug phase and will prevent some possible errors using the last function used in the example.
## A word on free()
## A word on `free`
Usually variables are automatically de-allocated when their scope is destroyed, freeing the memory they were using.
This simple does not happen when you manually allocate memory using the `malloc`.
To prevent memory leaks in more complex programs and in order to not create garbage in the system you have to free the memory area recently used before terminating your code execution.
While this may be true for stack (automatic) variables, it does not hold for the heap, which is where memory allocated by `malloc` lives.
While memory is returned to the system once your program terminates, if your program runs for a long time or makes many allocations without freeing, your program will hog resources and potentially slow down your system!
```C
free(arrayPointer);
free(arrayPointer);
```
In the end you will understand for sure that checking `arrayPointer` value was necessary to prevent an error using the `free` function.
If `arrayPointer` value was equal to `NULL` you could have expirencied some kind of bug.
As a rule of thumb, there should be a `free` for every `malloc`. Note that you should free all the memory you allocated before you exit, and preferably when you are done with what you were using.
You can use tools like `valgrind` to check your programs memory usage and leakage.
## Other functions similar to malloc
Sometimes you need to not only reserve a new area of memory for your operations, you might also need to initialize all bytes to zero.
This is what `calloc` is used for.
In other cases you wish to resize the amount of memory a pointer points to. For example, if you have a pointer acting as an array of size `n` and you want to change it to an array of size `m`, you can use `realloc`.
There are four main memory-allocation library functions in C:
* `malloc`
* `calloc`
* `realloc`
* `free`
You've already seen `malloc`, which allocates some amount of bytes on the heap, and `free` which tells the memory allocator that it can reuse that segment.
### `calloc`: contiguous allocate
Suppose you need an array that you will read and write from (e.g. when implementing a hash map).
If you use malloc, there is a chance that you will have garbage data in your array since malloc does not set the memory it gives you.
This can be problematic if you expect the array to be empty.
This is where `calloc` comes to the rescue!
Calloc sets the memory it provides to zero and has a handy signature for when allocating arrays.
```C
int *arr = malloc(2 * sizeof(int));
arr[0] = 1;
arr[1] = 2;
arr = realloc(arr, 3 * sizeof(int));
arr[2] = 3;
// If you're doing this...
int *arr1 = malloc(sizeof(int) * 10);
if (arr)
memset(arr, 0, sizeof(int);
// ...you can do this instead!
int *arr2 = calloc(10, sizeof(int));
```
**Note:** the first approach is more flexible since you can set the "default" value to whatever you want via `memset`. `memset` is declared in `string.h`.
### `realloc`: reallocate
What happens when you need more memory for your array? Instead of manually allocating a new heap segment and copying over memory, you can use `realloc`.
`realloc` changes the size of an old allocation to the specified size.
For example:
```C
// Allocate an int array that can hold two elements.
int *arr = malloc(2 * sizeof(int));
arr[0] = 1;
arr[1] = 2;
// Make the array bigger so it can hold three elements.
arr = realloc(arr, 3 * sizeof(int));
arr[2] = 3;
// Realloc "copies" over the data from the previous memory to the new location for you.
assert(arr[0] == 1);
assert(arr[1] == 2);
assert(arr[2] == 3);
// You can also shrink your allocation.
arr = realloc(arr, sizeof(int));
assert(arr[0] == 1);
```
**Warning!** Always use the pointer that `realloc` returns as the new address; it's not guaranteed to be the same as the one you passed in!
## Common errors
The improper use of dynamic memory allocation can frequently be a source of bugs as you have seen before.
Most common errors are:
* Not checking for allocation failures
Memory allocation is not guaranteed to succeed, and may instead return a null pointer.
Using the returned value, without checking if the allocation is successful, invokes undefined behavior. This usually leads to crash (due to the resulting segmentation fault on the null pointer dereference), but there is no guarantee that a crash will happen so relying on that can also lead to problems.
* Memory leaks
Failure to deallocate memory using `free` leads to buildup of non-reusable memory, which is no longer used by the program.
* Logical errors
All allocations must follow the same pattern: allocation using `malloc`, usage to store data, deallocation using `free`. If you not follow this pattern usually segmentation fault errore will be given and the program will crash. These errors can be transient and hard to debug for example, freed memory is usually not immediately reclaimed by the system, and dangling pointers may persist for a while and appear to work.
- **Segmentation faults**
This means you tried to access memory you do not have access to. A common pointer to dereference that you do not have access to is the `NULL` pointer (`0x0`). This is why it is important to check your pointers before you use them when you cannot guarantee validity (such as from `malloc`).
- **Leaks**
If you do not free memory in a timley fashion (or at all), you can leak memory, which can make your program (and maybe even your system) slow!
- **Logical errors**
The general pattern is allocate, use, and free. If you deviate from this pattern, you cause many problems such as a double free or the aforementioned segfault.