Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Pointer to Pointer 2D Array
Lecture 52 of the free C programming course: allocate a fully dynamic pointer to pointer 2D array in C, where both rows and columns are decided at run time, and learn to free and resize it safely.
The last two lectures each had one fixed dimension. Now we remove both limits. A pointer to pointer 2D array in C, declared as int **g, lets the user choose the number of rows and the number of columns while the program runs. This is the most flexible way to build a dynamic 2D array in C, and it is also where most memory leaks are born, so we will build it carefully with helper functions.
This lecture of the free embedded C course keeps the style of the earlier ones: small original code, tested output, and habits that make embedded C programming safer.
What You Will Learn
- How
int **gworks as a table of row pointers - How to write
grid_newandgrid_freehelpers with correct rollback on failure - The right order for freeing: rows first, then the table
- How to grow a single row with
reallocwithout breaking the rest - When a single flat block is a better choice
Prerequisites
You need cp8_50 and cp8_51 from this free C tutorial, plus the earlier lectures on pointer to pointer (double pointers) and on malloc, calloc, realloc and free.
How a Pointer to Pointer 2D Array Works
The idea is two levels of allocation. Level one is a block that holds rows pointers, each of type int *. The address of that block is stored in g, whose type is int **, a pointer to an int *. Level two is one block of cols integers per row, and each of those addresses is stored in the level-one block.
Two levels: pointer table and row blocks
The expression g[r][c] is evaluated in two steps. g[r] reads the pointer stored in slot r of the table, and [c] then indexes into the row it points to. Unlike cp8_50, there is no multiplication of row length; there is a memory load instead.
| Method | Fixed size | Allocations | Contiguous | Best when |
|---|---|---|---|---|
| Pointer to array (cp8_50) | Columns | 1 | Yes | Column count is constant |
| Array of pointers (cp8_51) | Rows | rows | No | Few rows, variable lengths |
| Pointer to pointer (this lecture) | None | rows + 1 | No | Both sizes unknown, ragged rows |
Writing Safe Helper Functions
With rows + 1 allocations, error handling is the hard part. If row 5 fails, rows 0 to 4 and the table must be released before you return. Putting that logic into two small functions keeps main clean and the rules in one place.
Complete Example: Multiplication Table Grid
Save this program as grid_demo.c. It builds a table, fills it with a multiplication table, prints it, then grows row 1 by two elements.
#include <stdio.h>
#include <stdlib.h>
static int **grid_new(size_t rows, size_t cols)
{
int **g = malloc(rows * sizeof *g);
if (g == NULL)
return NULL;
for (size_t r = 0; r < rows; r++) {
g[r] = calloc(cols, sizeof **g);
if (g[r] == NULL) {
while (r-- > 0) /* undo the rows already built */
free(g[r]);
free(g);
return NULL;
}
}
return g;
}
static void grid_free(int **g, size_t rows)
{
if (g == NULL)
return;
for (size_t r = 0; r < rows; r++)
free(g[r]); /* rows first ... */
free(g); /* ... then the table */
}
static void grid_print(int **g, size_t rows, size_t cols)
{
for (size_t r = 0; r < rows; r++) {
for (size_t c = 0; c < cols; c++)
printf("%4d", g[r][c]);
putchar('\n');
}
}
int main(void)
{
size_t rows, cols;
printf("rows and columns? ");
if (scanf("%zu %zu", &rows, &cols) != 2 || rows == 0 || cols == 0) {
fprintf(stderr, "invalid size\n");
return 1;
}
int **g = grid_new(rows, cols);
if (g == NULL) {
fprintf(stderr, "out of memory\n");
return 1;
}
for (size_t r = 0; r < rows; r++)
for (size_t c = 0; c < cols; c++)
g[r][c] = (int)((r + 1) * (c + 1)); /* multiplication table */
puts("table:");
grid_print(g, rows, cols);
/* grow row 1 by two more elements without touching other rows */
if (rows > 1) {
size_t newcols = cols + 2;
int *tmp = realloc(g[1], newcols * sizeof *tmp);
if (tmp == NULL) {
fprintf(stderr, "realloc failed, row 1 unchanged\n");
} else {
g[1] = tmp;
g[1][cols] = -1;
g[1][cols + 1] = -1;
printf("row 1 after growing to %zu columns:", newcols);
for (size_t c = 0; c < newcols; c++)
printf(" %d", g[1][c]);
putchar('\n');
}
}
grid_free(g, rows);
return 0;
}
Build and Run
$ gcc -std=c11 -Wall -Wextra -Wpedantic -o grid_demo grid_demo.c
$ ./grid_demo
rows and columns? 3 4
table:
1 2 3 4
2 4 6 8
3 6 9 12
row 1 after growing to 6 columns: 2 4 6 8 -1 -1
Walking Through the Tricky Parts
- Table allocation:
malloc(rows * sizeof *g). Here*ghas typeint *, so the size is one pointer per row. - Row allocation:
calloc(cols, sizeof **g). Here**gis a singleint. - Rollback loop:
while (r-- > 0)compares first, then decrements, so it visits r-1 down to 0 and never touches the failed row. - Free order: if you free the table first, you lose the only copies of the row addresses and leak every row.
- Growing a row: only
g[1]changes. The other rows still holdcolselements, so after this step the grid is ragged and you must remember that row 1 is longer.
Correct free order
An Alternative: One Flat Block
If both dimensions are known before allocating and rows never change length, a single block is usually better than a pointer to pointer 2D array:
int *flat = calloc(rows * cols, sizeof *flat); /* check for NULL */
/* element (r, c) lives at flat[r * cols + c] */
free(flat);
It needs one allocation and one free, it is contiguous and cache friendly, and it can be passed straight to functions that expect a buffer, such as DMA or file I/O. The price is writing the index arithmetic yourself. Many embedded C programming teams wrap it in a tiny macro or inline function.
Sizing a Pointer to Pointer 2D Array
Before allocating, estimate what the structure will cost. For a 3 × 4 table of 4-byte integers on a 64-bit system, the data is 48 bytes, the table of row pointers is 3 × 8 = 24 bytes, and the allocator adds bookkeeping for four separate blocks (three rows and the table). A flat block would need only 48 bytes plus one block of bookkeeping. On a PC that difference is invisible; on a board with 64 KB of RAM and a thousand rows it is not.
Also think about who owns the row lengths. In the demo program every row started with the same width, so a single cols variable was enough. After you resize one row, a pointer to pointer 2D array has become ragged, and a single width no longer describes it. The usual fix is a small struct that keeps the table, the number of rows and an array of lengths together, so the information can never get out of step with the memory it describes.
Finally, remember that a pointer to pointer 2D array can be given to a function as int **g together with the sizes, which is convenient. That is exactly why this layout is common in code that reads matrices, parses text tables or builds lookup structures at start-up, before the real-time part of the firmware begins.
Common Mistakes with a Pointer to Pointer 2D Array
| Mistake | Result | Fix |
|---|---|---|
| Freeing the table before the rows | Leak of every row | Free rows, then the table |
Using sizeof(int) for the table | Table too small on 64-bit systems | Use sizeof *g |
| No rollback on partial failure | Leak when memory is low | Keep the cleanup inside the helper |
Passing int ** to a function expecting int [][4] | Compiler warning and wrong reads | They are different types |
| Forgetting row lengths after realloc | Out-of-bounds access | Store a length per row |
Warning: an int ** is not the same thing as a real 2D array. int a[3][4] cannot be passed where int ** is expected, because the array has no table of pointers behind it. Mixing them compiles with a warning at best and crashes at worst.
Best Practices, Performance and Security
- Performance: every access needs an extra load for the row pointer, and the rows may be scattered. For heavy numeric loops prefer the flat block.
- Security: sanitise
rowsandcolsfrom the user and keep an upper limit. A huge value is an easy denial-of-service on a small device. - Ownership: decide which function owns the memory. Allocate and free with a matching pair of functions, as
grid_newandgrid_freedo. - Dangling pointers: after
grid_free, set your pointer to NULL. - Verification: compile once with
-fsanitize=address,undefinedto prove there are no leaks or overruns.
Summary and Key Takeaways
int **gpoints to a table of row pointers, and each pointer owns a row block.- Allocation takes rows + 1 calls, so wrap it in helper functions with rollback.
- Free every row first and the table last.
- Rows can be resized or have different lengths, but you must track each length.
- A flat block with
r * cols + cis often simpler and faster.
You have now seen all three ways to create a dynamic 2D array in C. Next, we move to a new topic: functions also live in memory and have addresses. That is the start of function pointers, a core feature of embedded C programming. Continue this free C programming course.
Frequently Asked Questions
What does int **g mean in C?
It is a pointer to a pointer to int. In a dynamic 2D array it points to the table that stores one int pointer per row.
Why must rows be freed before the table?
The table is the only place that stores the row addresses. If you free it first, you can no longer reach the rows and they are leaked.
Is a pointer to pointer the same as a 2D array?
No. A real 2D array is one contiguous block with a fixed row width. A pointer to pointer is a table of separate row pointers.
How do I resize one row?
Call realloc on that row’s pointer using a temporary variable, and replace the row pointer only when realloc succeeds. Remember to store the new length.
When should I use a flat array instead?
When all rows have the same length and you want fewer allocations, better cache behaviour, or a buffer you can pass to hardware or file functions.
Can I use this approach on a microcontroller?
Yes if a heap exists and fragmentation is acceptable. Many bare-metal projects avoid many small allocations and use a fixed pool instead.
Learn Embedded C Programming for Free
Follow the full free embedded C course on EmbeddedPathashala: pointers, memory, functions, quizzes and interview preparation in one place.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments