What is Dynamic 2D Arrays in C- C Programming Course With Examples

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

PREV_LEC | NEXT_LEC

Dynamic 2D Arrays in C

Lecture 50 of the free C programming course: build a dynamic 2D array in C with one malloc call and a pointer to an array. A must-know skill for embedded C programming.

A dynamic 2D array in C lets you decide the number of rows while the program is running, instead of hard-coding a size and wasting RAM. On a microcontroller with 64 KB of memory, or on an embedded Linux board that captures sensor frames, that flexibility matters. In this lecture of our free C programming course you will learn the cleanest way to build such an array: allocate every row in a single block and access it through a pointer to an array.

This tutorial is part of the free embedded C course on EmbeddedPathashala, so every example is small, original, tested with a modern compiler, and written the way real embedded C programming code is written.

What You Will Learn

  • Why a dynamic 2D array in C is useful when the row count is only known at run time
  • How to declare a pointer to an array such as int (*frame)[4] and what the parentheses mean
  • How to allocate all rows with a single malloc and release them with a single free
  • How the compiler turns frame[r][c] into address arithmetic
  • Common mistakes, overflow checks and best practices for heap-based 2D data

Prerequisites

Before starting, you should be comfortable with malloc, calloc and free (lectures cp8_45 to cp8_49), with two-dimensional arrays from the Arrays chapter, and with the idea of a pointer to an array from earlier in this Pointers chapter. If any of these feel shaky, revisit them in this free C tutorial first.

Why Use a Dynamic 2D Array in C?

A normal array such as int grid[10][4] has its size fixed when you compile. That is fine for lookup tables, but many embedded problems do not know the size in advance:

  • A data logger stores a configurable number of samples, each sample holding readings from 4 ADC channels.
  • A camera driver allocates a buffer whose height depends on the selected resolution.
  • A test tool reads a matrix from a file whose row count is unknown until it is opened.

You could declare a huge array and hope it is big enough, but that wastes RAM. You could also use a variable-length array, but large VLAs live on the stack, and stack overflow is a classic cause of mysterious crashes on small devices. The heap, managed with malloc, is the natural place for a buffer whose size is decided at run time.

Pointer to an Array: Reading the Declaration

The trick behind this technique is a declaration that confuses almost every beginner:

int (*frame)[4];

Read it from the inside out. frame is a pointer (because of (*frame)), and it points to an array of 4 ints. The parentheses are essential because the subscript operator [] binds tighter than *. Remove them and the meaning changes completely, as this comparison shows.

DeclarationMeaningsizeof on a 64-bit machine
int frame[4]Array of 4 ints16 bytes
int *frame[4]Array of 4 pointers to int32 bytes
int (*frame)[4]One pointer to an array of 4 ints8 bytes

The important consequence: when frame points to an array of 4 ints, the compiler treats frame + 1 as “move forward by one whole row”, which is 16 bytes. That is exactly what we need to walk a 2D table.

One pointer, one contiguous block of rows

frame → row 0: 0102030
frame + 1 → row 1: 100110120130
frame + 2 → row 2: 200210220230

Allocating a Dynamic 2D Array in C

Because every row has the same length (4 columns), the whole table is just rows × 4 integers placed back to back. One allocation is enough. The key idiom is malloc(rows * sizeof *frame): *frame is one row, so sizeof *frame is the size of one row, and you never have to repeat the type or the number 4.

Modern C note: old textbooks include <alloc.h> and cast the result of malloc. Today the header is <stdlib.h>, and in C you should not cast the result of malloc, because the cast can hide a missing header. Also write int main(void) and check for NULL.

Complete Example: ADC Frame Buffer

Save this program as frame_buffer.c. It stores fake ADC counts for 4 channels, one row per sample, and the number of samples is typed by the user.

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

#define CHANNELS 4

int main(void)
{
    size_t samples;

    printf("How many sample rows? ");
    if (scanf("%zu", &samples) != 1 || samples == 0) {
        fprintf(stderr, "invalid row count\n");
        return 1;
    }

    /* one allocation: 'samples' rows, each row is int[CHANNELS] */
    int (*frame)[CHANNELS] = malloc(samples * sizeof *frame);
    if (frame == NULL) {
        perror("malloc");
        return 1;
    }

    for (size_t r = 0; r < samples; r++)
        for (size_t c = 0; c < CHANNELS; c++)
            frame[r][c] = (int)(r * 100 + c * 10);   /* fake ADC counts */

    puts("ADC frame buffer:");
    for (size_t r = 0; r < samples; r++) {
        for (size_t c = 0; c < CHANNELS; c++)
            printf("%6d", frame[r][c]);
        putchar('\n');
    }

    printf("sizeof *frame      = %zu bytes (one row)\n", sizeof *frame);
    printf("total allocation   = %zu bytes\n", samples * sizeof *frame);
    printf("row 0 -> row 1 gap = %td bytes\n",
           (char *)&frame[1] - (char *)&frame[0]);

    free(frame);
    return 0;
}

Build and Run

$ gcc -std=c11 -Wall -Wextra -Wpedantic -o frame_buffer frame_buffer.c
$ ./frame_buffer
How many sample rows? 3
ADC frame buffer:
     0    10    20    30
   100   110   120   130
   200   210   220   230
sizeof *frame      = 16 bytes (one row)
total allocation   = 48 bytes
row 0 -> row 1 gap = 16 bytes

Three rows of four 4-byte integers give 48 bytes, and the distance between two neighbouring rows is exactly one row, 16 bytes. The numbers can differ on a platform where int is not 4 bytes, but the relationship stays the same.

How the Compiler Reads frame[r][c]

For the expression frame[r][c] the compiler performs two steps. First frame[r] means *(frame + r): add r rows (each 16 bytes) to the base address and you land on the start of row r, which is itself an array of 4 ints. Then [c] picks one int inside that row. In plain arithmetic the byte offset is (r * 4 + c) * sizeof(int). You write two simple subscripts; the compiler does the multiplication.

Address arithmetic for frame[1][2]

step 1 → frame + 1 = base + 16 bytes
step 2 → element 2 = + 2 x 4 bytes
result → base + 24 bytes = value 120

Real-World Uses in Embedded Systems

  • Multi-channel ADC capture: each row is one scan of all channels, which matches how DMA delivers data.
  • Image or display lines: a fixed width with a configurable height.
  • Test fixtures on a host PC: load a table of expected values of unknown length.

On a bare-metal microcontroller, heap use is often avoided because of fragmentation. In that case the same pointer-to-array type still helps: point it at a statically reserved pool and you keep the clean frame[r][c] syntax. This is one reason a good free embedded C course teaches this declaration properly.

Common Mistakes with a Dynamic 2D Array in C

MistakeWhat goes wrongFix
Writing int *frame[4]You declared 4 pointers, not a pointer to a rowUse int (*frame)[4]
Allocating rows * sizeof(int)Buffer is 4 times too small, heap corruptionUse rows * sizeof *frame
Skipping the NULL checkCrash when memory runs outAlways test the result of malloc
Calling free on each rowUndefined behaviour: rows are not separate blocksCall free once on frame
Using the pointer after freeUse-after-free bugSet it to NULL after freeing

Warning: rows * sizeof *frame can overflow if rows is huge or comes from untrusted input. A wrapped value allocates a small buffer that your loops then overrun. Use calloc(rows, sizeof *frame), which checks the multiplication for you, or compare rows against SIZE_MAX / sizeof *frame first.

Best Practices, Performance and Security

  • Performance: one contiguous block is cache friendly. Walking row by row touches memory in order, which is much faster than jumping between separate allocations.
  • One allocation, one free: fewer calls, less fragmentation, and no partial-failure cleanup logic.
  • Use size_t for sizes and indices, and print them with %zu.
  • Validate input before allocating, and reject zero or absurd values.
  • Tool check: build once with -fsanitize=address,undefined during development to catch overruns and leaks.
  • Variable-length types: when the column count is also a run-time value, C99 allows int (*frame)[cols] on compilers that support variably modified types. Use it only when your toolchain allows it.

Summary and Key Takeaways

  • A pointer to an array, int (*p)[N], steps one whole row at a time.
  • When only the row count is dynamic, one malloc(rows * sizeof *p) builds the whole 2D array.
  • Access elements with normal p[r][c] syntax.
  • Release everything with a single free(p).
  • Check for NULL and guard against size overflow.

You now know the simplest and fastest way to build a dynamic 2D array in C. In the next lecture we flip the situation: the number of rows is fixed and the number of columns is chosen at run time, which needs an array of pointers instead. Continue with the rest of this free C tutorial to master C programming for embedded systems.

Frequently Asked Questions

What is a dynamic 2D array in C?

It is a two-dimensional table whose size is decided at run time and stored on the heap, usually with malloc or calloc, instead of being fixed at compile time.

Why does int (*p)[4] need parentheses?

Because the subscript operator has higher precedence than the dereference operator. Without parentheses you get an array of four pointers.

Do I need to cast the return value of malloc in C?

No. A void pointer converts implicitly in C, and an unnecessary cast can hide a missing stdlib.h include.

How many times should I call free for this 2D array?

Exactly once, on the pointer returned by malloc, because all rows live in one block.

Is it safe to use dynamic memory in embedded C?

It depends on the system. On embedded Linux it is normal. On small bare-metal targets many teams prefer static pools to avoid fragmentation, but the pointer-to-array syntax still applies.

Can I use this when the column count is also unknown?

Not with a fixed array type. Use a variably modified type if your compiler supports it, or the pointer-to-pointer method covered later in this chapter.

Keep Learning with the Free Embedded C Course

EmbeddedPathashala offers a complete free C programming course, from basics to pointers and embedded C programming, with quizzes and interview questions.

PREV_LEC | NEXT_LEC

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *