Hello! Welcome to your next lesson in the "Complex Pointer Types and Multi-Level Indirection" module.
In our last session, we mastered the essential technique of using a pointer to a pointer (**) to modify a caller's pointer from within a function. This is the key that unlocks many advanced dynamic memory patterns in C.
Today, we will put that knowledge into immediate, practical use. Our objective is to implement a dynamic two-dimensional array using a pointer to a pointer. This is a fundamental skill for creating flexible data structures where the dimensions are not known at compile time. In your work with AUTOSAR and communication stacks, you often deal with data structures like service discovery tables or message payloads whose size can vary. This technique is a cornerstone for managing such dynamic data in C.
The Challenge of Dynamic 2D Arrays
With a standard 2D array like int matrix[10][20];, the compiler must know both dimensions at compile time to allocate a single, contiguous block of memory. But what if you need a 100x500 matrix for one task and a 5x15 matrix for another, with the sizes determined by runtime configuration or user input? This is where dynamic allocation becomes necessary.
We will implement this using an "array of pointers" model. Here's the concept:
- We will have a pointer to a pointer, for example,
int **matrix. - This
matrixpointer will point to a dynamically allocated array ofint *pointers. - Each pointer in that array (
matrix[0],matrix[1], etc.) will, in turn, point to its own dynamically allocated array ofints. This inner array represents a single row of our 2D matrix.
This structure gives us two levels of indirection, which is why a double pointer is the perfect tool.

Step-by-Step Implementation
Let's walk through the four essential steps: allocation, access, deallocation, and wrapping it in functions.
1. Allocation: Building the Structure
Allocation is a two-stage process. Imagine you have int rows and int cols.
Stage 1: Allocate the array for the row pointers.
First, we need to allocate memory for an array that will hold the pointers to each row. The main matrix pointer will point to this.
int **matrix;
matrix = (int **)malloc(rows * sizeof(int *));
- We are allocating memory for
rowsnumber of elements. - The size of each element is
sizeof(int *), because this array will hold pointers, not integers. - The
malloccall returns avoid *which we cast toint **, the type of ourmatrixvariable.
Stage 2: Allocate memory for each individual row.
Now, we must iterate through the array of pointers we just created and make each one point to a newly allocated block of memory for the columns.
for (int i = 0; i < rows; i++) {
matrix[i] = (int *)malloc(cols * sizeof(int));
}
- For each row
i, we allocate memory forcolsnumber of integers. - The size of each element is
sizeof(int). - The address of this new block is assigned to
matrix[i].matrix[i]now holds the base address of rowi.
After these two stages, we have successfully constructed our dynamic 2D array in memory.
2. Accessing Elements: The [i][j] Syntax
The beauty of this approach is that after setting it up, you can use the familiar and intuitive [i][j] syntax to access elements, just as with a static 2D array.
matrix[2][5] = 100; // Assign 100 to the element at row 2, column 5
int value = matrix[2][5];
It's crucial to understand how the compiler interprets this. Let's break down matrix[i][j]:
matrix: A variable of typeint **. It holds the address of the start of the pointer array.matrix[i]: This dereferences thematrixpointer to get the i-th element of the pointer array. This element itself is of typeint *and holds the base address of the i-th row.matrix[i][j]: This takes the row pointer from the previous step and dereferences it to get the j-th integer in that row. The final type isint.
While the [][] syntax is convenient, it's equivalent to pointer arithmetic. Understanding this is key to mastery.

3. Deallocation: Cleaning Up in Reverse
Memory leaks are a critical bug in any system, and they are especially common with complex dynamic structures. For every malloc, there must be a corresponding free.
With our N+1 calls to malloc (1 for the pointer array + N for the rows), we must perform N+1 calls to free in the exact reverse order of allocation.
Stage 1: Free each individual row.
First, we loop through and free the memory allocated for each row.
for (int i = 0; i < rows; i++) {
free(matrix[i]);
}
Stage 2: Free the array of pointers.
Only after all the rows have been freed can we free the memory that was holding the pointers to them.
free(matrix);
If you free matrix first, you lose the pointers to all the rows, making it impossible to free them and creating a massive memory leak.
A Complete Walkthrough
The following video provides an excellent, comprehensive tutorial that covers everything we just discussed: conceptualization, allocation, memory layout, and deallocation. It will solidify your understanding of the entire process.
Dynamically Allocate A 2D Array | C Programming Tutorial
This video, 'Dynamically Allocate A 2D Array' from Portfolio Courses, walks through the entire process of creating and managing a dynamic 2D array.
Watch the video from the beginning to the end (around 15 minutes). Pay close attention to: The initial step-by-step allocation (01:24) and how it's refined into a loop (05:51). The detailed memory visualization (07:51), which contrasts this dynamic method with a static stack-allocated array. This highlights the memory overhead from the pointers, a key consideration in embedded systems. The correct procedure for freeing memory (13:51).
Test your understanding!
Write two functions:
int** create_matrix(int rows, int cols): This function should dynamically allocate a 2D array of the given dimensions, perform error checking formalloc, and return theint**pointer. If any allocation fails, it should clean up any memory already allocated and returnNULL.void free_matrix(int** matrix, int rows): This function should correctly deallocate all memory associated with the matrix.
Use these functions in main to create, populate, print, and then free a 4x5 matrix.
Show answer
#include <stdio.h>
#include <stdlib.h>
// Function to allocate and create a 2D matrix
int** create_matrix(int rows, int cols) {
int** matrix = (int**)malloc(rows * sizeof(int*));
if (matrix == NULL) {
printf("Error: Failed to allocate memory for row pointers.\n");
return NULL;
}
for (int i = 0; i < rows; i++) {
matrix[i] = (int*)malloc(cols * sizeof(int));
if (matrix[i] == NULL) {
printf("Error: Failed to allocate memory for row %d.\n", i);
// Cleanup previously allocated rows before failing
for (int j = 0; j < i; j++) {
free(matrix[j]);
}
free(matrix); // Free the pointer array
return NULL;
}
}
return matrix;
}
// Function to free the memory of the 2D matrix
void free_matrix(int** matrix, int rows) {
if (matrix == NULL) {
return;
}
// First, free each individual row
for (int i = 0; i < rows; i++) {
free(matrix[i]);
}
// Finally, free the array of pointers
free(matrix);
}
int main(void) {
int rows = 4;
int cols = 5;
int** my_matrix = create_matrix(rows, cols);
if (my_matrix == NULL) {
return 1; // Exit if creation failed
}
printf("Matrix created successfully. Populating and printing:\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
my_matrix[i][j] = i * 10 + j;
printf("%-4d", my_matrix[i][j]);
}
printf("\n");
}
// Clean up
free_matrix(my_matrix, rows);
my_matrix = NULL; // Good practice
printf("\nMatrix freed.\n");
return 0;
}
Performance and Memory Layout: A Deeper Look
Your background in embedded systems makes you keenly aware of performance and memory constraints. The "array of pointers" method is programmer-friendly but has two important characteristics:
- Memory Overhead: It requires extra memory to store the array of pointers. On a 64-bit system, if you have 100 rows, you use an additional 800 bytes (
100 * sizeof(int*)) just for the pointers, on top of the data itself. - Non-Contiguous Rows: The
malloccalls for each row do not guarantee that rowiand rowi+1are adjacent in memory. This can lead to poorer cache performance on large matrices, as accessing consecutive rows might involve jumping to different memory locations (cache misses).
There is an alternative method that addresses these issues, which is excellent to have in your toolkit. The following resource discusses both our primary method and this alternative.
A TUTORIAL ON POINTERS AND ARRAYS IN C by Ted ...
The document 'A TUTORIAL ON POINTERS AND ARRAYS IN C' by Ted Jensen provides a professional-level discussion of different dynamic allocation strategies. It's a great way to deepen your understanding.
Read 'METHOD 3' and 'METHOD 4' in Chapter 9 (pages 36-39). METHOD 3 describes exactly the 'array of pointers' technique we've implemented in this lesson. Notice how it highlights the non-contiguous nature of the data. METHOD 4 presents an alternative: first, allocate one large, contiguous block for all the data (nrows * ncols). Then, allocate the array of pointers, but instead of calling malloc for each row, you manually calculate and assign the starting address of each row within the large block. This gives you both the convenience of [i][j] access and the performance benefit of contiguous memory.
This second method trades a slightly more complex setup for better cache locality and fewer malloc calls, a trade-off that is often worthwhile in performance-critical embedded applications.
Conclusion
You have now learned how to implement one of the most common and flexible C data structures: a truly dynamic 2D array. You can create it, access it, and—most importantly—destroy it without memory leaks.
Key Takeaways:
- A dynamic 2D array can be implemented as an array of pointers, managed by a pointer to a pointer (
int **). - Allocation is a two-stage process: first allocate the array of row pointers, then allocate each row individually.
- Deallocation must happen in the reverse order: free each row first, then free the array of pointers.
- This method allows for
[i][j]syntax but results in non-contiguous memory for the rows and incurs overhead for storing the row pointers. - An alternative method exists to create a contiguous data block while still using a pointer-to-pointer interface, offering better performance at the cost of a more complex setup.
Preview of the Next Lesson:
The "array of pointers" method we learned today has one more powerful advantage: not all rows need to have the same number of columns. In our next lesson, we will explore this by learning how to implement a jagged array using an array of pointers.