Understanding Dynamic Memory Allocation in C

Understanding Dynamic Memory Allocation in C

Understanding Dynamic Memory Allocation in C


In the C language, memory management is a crucial aspect. So far, you’ve probably worked with variables whose size is known at compile time (static allocation) or with arrays of fixed size.

However, in many situations, you don’t know exactly how much memory you’ll need until the program is running. This is where dynamic memory allocation comes in.

Dynamic allocation allows you to request memory from the operating system while the program is running and free it when you no longer need it. This provides flexibility and efficiency in using system resources.


Why Do We Need Dynamic Allocation?

  • Unknown sizes at compile time: You don’t know how many elements you’ll need to store (for example, reading from a file or user input).
  • Efficient memory usage: You allocate memory only when needed and release it when no longer required, avoiding waste.
  • Complex data structures: Allows creating data structures such as linked lists, trees, graphs, where size and structure change during execution.

Dynamic Allocation Functions in C

In C, dynamic memory allocation is performed using standard functions from the <stdlib.h> library:

  1. malloc() (Memory Allocation)
  2. calloc() (Contiguous Allocation)
  3. realloc() (Reallocation)
  4. free() (Freeing memory)

1. malloc()

The malloc() function allocates a block of memory of a certain size (in bytes) and returns a pointer of type void * (a generic pointer) to the beginning of the allocated block. If allocation fails (for example, not enough memory available), malloc() returns NULL.

Syntax:

void* malloc (size_t size);

size: Number of bytes to allocate.


Example: Allocating a single integer

#include <stdio.h>
#include <stdlib.h> // For malloc and free

int main() {
  int *ptr_int; // Declare a pointer to int

  // Allocate memory for a single int
  ptr_int = (int*) malloc(sizeof(int)); // sizeof(int) gives size in bytes of an int

  // Check if allocation succeeded
  if (ptr_int == NULL) {
    printf("Memory allocation error!\n");
    return 1; // Exit with error code
  }

  // Now we can use the allocated memory
  *ptr_int = 100;
  printf("Dynamically allocated value: %d\n", *ptr_int);

  // Free the allocated memory
  free(ptr_int);
  ptr_int = NULL; // Good practice: set pointer to NULL after freeing

  return 0;
}

Explanation:

  • Declared a pointer to int.
  • Called malloc(sizeof(int)) to allocate memory for an integer. sizeof(int) ensures portability across systems.
  • Explicit cast to (int*) – though implicit in C, it’s good practice for clarity and C++ compatibility.
  • Checked if allocation returned NULL.
  • Used the dereference operator (*) to access/modify the allocated memory.
  • Freed the memory with free(ptr_int) to avoid leaks.
  • Set pointer to NULL to prevent dangling pointer issues.

Example: Allocating an array of integers

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

int main() {
  int *table;
  int n, i;

  printf("Enter number of elements: ");
  scanf("%d", &n);

  // Allocate memory for n integers
  table = (int*) malloc(n * sizeof(int));

  if (table == NULL) {
    printf("Error allocating memory for array!\n");
    return 1;
  }

  // Read elements into the array
  printf("Enter %d elements:\n", n);
  for (i = 0; i < n; i++) {
    scanf("%d", &table[i]);
  }

  // Print the elements
  printf("The entered elements are:\n");
  for (i = 0; i < n; i++) {
    printf("%d ", table[i]);
  }
  printf("\n");

  // Free allocated memory
  free(table);
  table = NULL;

  return 0;
}

Explanation:

  • Read the number of elements (n) from the user.
  • Allocated memory for n integers with malloc(n * sizeof(int)).
  • Used the pointer like an array (table[i]).
  • Freed memory at the end.

2. calloc()

The calloc() function allocates a block of memory for a specified number of elements, each with a given size. A major advantage is that it initializes all allocated bytes to zero. Like malloc(), it returns NULL on failure.

Syntax:

void* calloc (size_t num, size_t size);

num: Number of elements. size: Size (in bytes) of each element.


Example: Allocating an array of integers initialized to zero

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

int main() {
  int *table;
  int n, i;

  printf("Enter number of elements: ");
  scanf("%d", &n);

  // Allocate memory for n integers and initialize them to zero
  table = (int*) calloc(n, sizeof(int));

  if (table == NULL) {
    printf("Error allocating memory for array!\n");
    return 1;
  }

  // Display initial elements (all zeros)
  printf("Initial elements (initialized with 0):\n");
  for (i = 0; i < n; i++) {
    printf("%d ", table[i]);
  }
  printf("\n");

  // Free allocated memory
  free(table);
  table = NULL;

  return 0;
}

Explanation:

  • calloc(n, sizeof(int)) is equivalent to malloc(n * sizeof(int)), but memory is zero-initialized.
  • Useful when you need a default zero value (arrays, structs).

3. realloc()

The realloc() function attempts to resize a previously allocated memory block. It can expand or shrink it.

Possible scenarios:

  • If enough space is available, the block may expand in place.
  • Otherwise, realloc() allocates a new block, copies old content, and frees the old block.
  • On failure, realloc() returns NULL, and the original block remains unchanged.

Syntax:

void* realloc (void* ptr, size_t size);

ptr: Pointer to previously allocated memory. May be NULL (acts like malloc()). size: New size in bytes. If zero, behaves like free().


Example: Resizing an array

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

int main() {
  int *table;
  int n_initial, n_nou, i;

  printf("Enter initial number of elements: ");
  scanf("%d", &n_initial);

  // Allocate initial memory
  table = (int*) malloc(n_initial * sizeof(int));

  if (table == NULL) {
    printf("Error allocating initial memory!\n");
    return 1;
  }

  printf("Enter %d initial elements:\n", n_initial);
  for (i = 0; i < n_initial; i++) {
    scanf("%d", &table[i]);
  }

  printf("Initial elements:\n");
  for (i = 0; i < n_initial; i++) {
    printf("%d ", table[i]);
  }
  printf("\n");

  printf("Enter new number of elements: ");
  scanf("%d", &n_nou);

  // Resize the array
  int *temp_table = (int*) realloc(table, n_nou * sizeof(int));

  if (temp_table == NULL) {
    printf("Error resizing memory!\n");
    free(table); // Original block remains valid
    return 1;
  } else {
    table = temp_table; // Update pointer
  }

  // If expanded, add new elements
  if (n_nou > n_initial) {
    printf("Enter additional elements:\n");
    for (i = n_initial; i < n_nou; i++) {
      scanf("%d", &table[i]);
    }
  }

  printf("Elements after resizing:\n");
  for (i = 0; i < n_nou; i++) {
    printf("%d ", table[i]);
  }
  printf("\n");

  // Free memory
  free(table);
  table = NULL;

  return 0;
}

Explanation:

  • Used a temporary pointer to avoid losing the original block if realloc fails.
  • If successful, reassigned to table.
  • Added new elements when expanded.
  • Freed memory at the end.

4. free()

The free() function is used to release memory previously allocated by malloc(), calloc(), or realloc().

Syntax:

void free (void* ptr);

ptr: Pointer to the block to free. If NULL, free() does nothing.


Important Rules for free():

  • Free only dynamically allocated memory.
  • Do not free the same block more than once.
  • free(NULL) is safe, but setting the pointer to NULL after freeing is best practice.
  • Always avoid dangling pointers.

Correct usage of free():

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

int main() {
  int *data = NULL; // Initialize pointer to NULL
  int size = 5;

  // Allocate memory
  data = (int*) malloc(size * sizeof(int));

  if (data != NULL) {
    // Use memory
    for (int i = 0; i < size; i++) {
      data[i] = i * 10;
      printf("%d ", data[i]);
    }
    printf("\n");

    // Free memory
    free(data);
    data = NULL;
  } else {
    printf("Memory allocation failed!\n");
  }

  // Free again (safe because data is NULL)
  free(data);

  return 0;
}

Memory Leaks

A memory leak occurs when dynamically allocated memory is not freed before the program ends. This can exhaust available memory and, in long-running programs (like servers), seriously impact performance.


Example of a memory leak:

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

int main() {
  int *data;
  int size = 1000;

  // Allocate memory
  data = (int*) malloc(size * sizeof(int));

  if (data != NULL) {
    // Use memory...
    // ... but forget to free(data);
  }

  // Program ends, but malloc-allocated memory is not explicitly freed.
  // The OS will reclaim it, but in long-running programs this is a problem.

  return 0; // Memory leak here!
}

You need to be logged in to access the cloud lab and experiment with the code presented in this tutorial.

Log in