A structured lesson workspace with readable content, hands-on examples, and a clean path to completion.
A static 2D array int grid[3][4] is a single contiguous block of 12 ints. The compiler handles the row-major indexing. A dynamic 2D array requires you to make a design decision: allocate an array of row pointers, each pointing to a separately allocated row, or allocate one flat block and compute indices manually. The first gives you arr[i][j] syntax. The second gives you cache locality and a single free. Both are correct. They have different trade-offs.
#include <stdio.h>#include <stdlib.h>int main(void) {int rows = 3, cols = 4;int **grid = malloc(rows * sizeof(int *));if (grid == NULL) return 1;for (int i = 0; i < rows; i++) {grid[i] = malloc(cols * sizeof(int));if (grid[i] == NULL) {for (int j = 0; j < i; j++) free(grid[j]);free(grid);return 1;}}int val = 1;for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {grid[i][j] = val++;}}for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {printf("%3d ", grid[i][j]);}printf("\n");}for (int i = 0; i < rows; i++) free(grid[i]);free(grid);return 0;}
Four allocations total: one for the row pointer array, three for the rows. Four frees in reverse order. The cleanup loop is easy to get wrong. Miss one free and you leak. Free in the wrong order and you corrupt the heap. The error-handling path during allocation must free everything allocated so far, which is why the inner NULL check has its own cleanup loop.
#include <stdio.h>#include <stdlib.h>#define IDX(i, j, cols) ((i) * (cols) + (j))int main(void) {int rows = 3, cols = 4;int *grid = malloc(rows * cols * sizeof(int));if (grid == NULL) return 1;int val = 1;for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {grid[IDX(i, j, cols)] = val++;}}for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {printf("%3d ", grid[IDX(i, j, cols)]);}printf("\n");}free(grid);return 0;}
One allocation. One free. The IDX macro computes the row-major offset. The data is contiguous, which means better cache performance for row-wise traversal. The downside: you lose the arr[i][j] syntax and must use the macro or inline arithmetic everywhere. For performance-critical code (image processing, matrix operations), the flat buffer wins. For readability and convenience, the row-pointer approach wins.
Dynamic 2D arrays appear in image processing, game boards, spreadsheet engines, and numerical computation. The choice between row-pointers and flat buffers is a design decision you make once and live with. Next: the int *p vs int* p declaration style debate, which is less about correctness and more about avoiding a specific beginner mistake.
Finish the lesson once you have worked through the material. This awards ★ 40 XP.