Dynamic Memory
Dynamic data (as opposed to static data) is data that may grow or shrink in size during program execution. The number of orders currently being processed at your local catalogue shop, for example, can fluctuate quite drastically. On a typical Saturday, the number might grow considerably as the day progresses, and then decline sharply towards the close of business. Orders are held in a queue. New orders placed will be added to the end (or tail) of the queue, while old orders are deleted from the front (or head) of the queue as they are collected by customers.
Assuming that we are writing a program to deal with this kind of scenario, it will obviously be advantageous to be able to allocate additional memory space as and when it is required, as opposed to simply allocating a very large static array of user-defined variables to hold potential orders, which is wasteful and inefficient. The use of dynamic memory allocation techniques will ensure that the program only grabs sufficient memory to satisfy its immediate needs, rather than holding on to large chunks of memory that could be more usefully employed elsewhere.
The functions provided by C for the dynamic allocation (and de-allocation) of memory are declared in the stdlib.h header file, and are described in the following table:
Function | Declaration | Description |
---|---|---|
malloc() | void *malloc(size_t size); | Allocates size bytes of memory. If successful, a pointer to the block of memory is returned, otherwise a null pointer is returned. |
calloc() | void *calloc(size_t num_elements, size_t element_size); | Allocates and initialises memory to zero (unlike malloc(), which does not initialise the memory allocated. If successful, a pointer to the block of memory is returned, otherwise a null pointer is returned. |
free() | void free(void *pointer); | Releases the block of memory pointed to by pointer (pointer must have previously been returned by malloc(), calloc(), or realloc(). |
realloc() | void *realloc(void *pointer, size_t size); | Resizes an existing block of dynamically allocated memory (which is truncated to the new size, if smaller than the original). If realloc() cannot resize the original memory block, it will allocate a new memory block, copy data from the original block to the new block, and free the original memory and its pointer. If realloc() fails altogether, it returns a null pointer and leaves the original memory block untouched. |
All of the functions return a pointer of type void, which can subsequently be type cast to any type for the purposes of performing pointer arithmetic. The size_t argument is an unsigned type, and is defined in stdlib.h. The code fragment below creates a pointer to type int (int_ptr), allocates a block of contiguous memory large enough to hold 25 variables of type int, and assigns the base address of the block to int_ptr.
Note the use of the sizeof() function, which enables the compiler to allocate the correct amount of memory for each integer variable (you could simply specify the amount of memory required in bytes, but the use of the sizeof() function ensures that the correct amount of memory will be allocated on different platforms, even if the size of type int on those platforms is different). The sizeof() function can be used to find the size of any variable, even a user-defined type such as a structure.
int *int_ptr;
int_ptr = (int *) malloc(25*sizeof(int));
Thanks to pointer arithmetic, it is possible to treat the reserved block of memory as an array of variables (which is essentially exactly what it is). We can use pointer values to index into the array, just as we can use array subscripts to access an array element. Both of the following statements, for example, allocate a value to the tenth integer variable in the memory block pointed at by int_ptr:
int_ptr[9] = 100;
*int_ptr + 9 = 100;
Whenever malloc() (or calloc() or realloc()) is used to allocate a block of memory dynamically, the memory should always be released once the program no longer needs it, so that it is available to other applications. The function free() takes one argument – the address of the block of memory to be freed (in other words the pointer variable that the address was assigned to when the memory was first allocated). To release the memory allocated in the example above, we could use the following statement:
free(int_ptr);
The short example program below asks the user to enter the number of integers that should be stored in an array for which the memory will be allocated dynamically using malloc(). It will then print out the numbers stored in the array, and then release the memory for the array using free().
// Example program 1
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
void main()
{
int num;
int *num_ptr;
int i;
printf("How many integer values do you want to allocate? ");
scanf("%d", &num);
num_ptr = malloc(num*sizeof(int));
if(num_ptr!=NULL)
{
printf("\n\n");
for(i=0; i<num; i++) *(num_ptr+i) = i;
for(i=num; i>0; i--) printf("%d\n", *(num_ptr+(i-1)));
free(num_ptr);
}
else
{
printf("\nInsufficient memory.\n\n");
}
printf("\n\nPress any key to exit . . . ");
getch();
}
The output from example program 1 is shown below.
The output from example program 1
If a block of memory that has already been allocated and populated with data is increased in size using realloc(), the contents are unaffected (additional space is simply added to the end of the existing block of memory). If the block will actually become smaller as a result of the change in size, any data in the original block will be truncated, but will otherwise remain unchanged.
If for some reason the realloc() function cannot extend the size of the original block of memory in its original location, it will simply create a new block of memory of the required size and copy the contents of the old block into the new one, before returning a new pointer value to reflect the memory block’s change of address. The original block of memory is returned to the "heap" (the name used to describe all of the computer’s free memory at any given time) for use by other programs. If realloc() fails altogether for some reason, it returns a null pointer and the original block of memory remains unaffected.
To change the amount of memory allocated to the pointer *int_ptr in the example given earlier, we could use the following statement:
int_ptr = (int *) realloc(int_ptr, 50);
The realloc() function takes two arguments. The first is a pointer to the block of memory to be re-sized, and the second is the total number of bytes of memory required. If successful, realloc() returns a void pointer. If not, it returns a NULL pointer. The short program below uses malloc() to allocate memory for an array of six integers, and subsequently calls realloc() to double the size of the array to twelve integers.
// Example program 2
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <math.h>
void main()
{
int *num_ptr;
int i;
num_ptr = malloc(6*sizeof(int));
if(num_ptr!=NULL)
{
for(i=0; i<6; i++) *(num_ptr+i) = pow(2, i); /* successive powers of 2 */
num_ptr = realloc(num_ptr, 12*sizeof(int));
if(num_ptr!=NULL)
{
for(i=6; i<12; i++) *(num_ptr+i) = pow(2, i);
for(i=0; i<12; i++) printf("num_ptr[%d] holds %d\n", i, num_ptr[i]);
free(num_ptr);
}
else printf("Insufficient memory.\n");
}
else printf("Insufficient memory.\n");
printf("\n\nPress any key to exit . . . ");
getch();
}
The output from example program 2 is shown below.
The output from example program 2
The calloc() function is similar to malloc(), except that the memory allocated by calloc() is initialised to zero, whereas the memory allocated by malloc() is not, and could therefore contain random values. The other difference is that calloc() requires two arguments. The first is the number of variables for which memory is required, and the second is the size of each variable. Like malloc(), calloc() returns a void pointer if the memory allocation succeeds, or a NULL pointer if it fails.