A structured lesson workspace with readable content, hands-on examples, and a clean path to completion.
Resizing a 2D array is harder than resizing a 1D array because the row-pointer structure has two levels of indirection. Adding rows means reallocating the pointer array and allocating new row buffers. Adding columns means reallocating every existing row. Both operations must preserve existing data. Both have failure paths that require cleanup of partially completed work. This is the kind of code where a single missed free leaks memory and a single missed NULL check crashes the program.
#include <stdio.h>#include <stdlib.h>#include <string.h>typedef struct {int **rows;int num_rows;int num_cols;} Grid;int grid_init(Grid *g, int rows, int cols) {g->rows = malloc(rows * sizeof(int *));if (!g->rows) return -1;for (int i = 0; i < rows; i++) {g->rows[i] = calloc(cols, sizeof(int));if (!g->rows[i]) {for (int j = 0; j < i; j++) free(g->rows[j]);free(g->rows);return -1;}}g->num_rows = rows;g->num_cols = cols;return 0;}int grid_add_row(Grid *g) {int **tmp = realloc(g->rows, (g->num_rows + 1) * sizeof(int *));if (!tmp) return -1;g->rows = tmp;g->rows[g->num_rows] = calloc(g->num_cols, sizeof(int));if (!g->rows[g->num_rows]) return -1;g->num_rows++;return 0;}void grid_free(Grid *g) {for (int i = 0; i < g->num_rows; i++) free(g->rows[i]);free(g->rows);g->rows = NULL;g->num_rows = 0;g->num_cols = 0;}int main(void) {Grid g;grid_init(&g, 2, 3);g.rows[0][0] = 1; g.rows[0][1] = 2; g.rows[0][2] = 3;g.rows[1][0] = 4; g.rows[1][1] = 5; g.rows[1][2] = 6;printf("Before (%dx%d):\n", g.num_rows, g.num_cols);for (int i = 0; i < g.num_rows; i++) {for (int j = 0; j < g.num_cols; j++)printf("%d ", g.rows[i][j]);printf("\n");}grid_add_row(&g);g.rows[2][0] = 7; g.rows[2][1] = 8; g.rows[2][2] = 9;printf("\nAfter (%dx%d):\n", g.num_rows, g.num_cols);for (int i = 0; i < g.num_rows; i++) {for (int j = 0; j < g.num_cols; j++)printf("%d ", g.rows[i][j]);printf("\n");}grid_free(&g);return 0;}
The existing rows are untouched. The pointer array is extended by one slot. The new row is allocated and zero-initialized with calloc. The cleanup in grid_free walks all rows, then frees the pointer array. Every allocation has a corresponding free. Every failure path cleans up what was allocated so far. This discipline is not optional. It is the price of manual memory management.
Dynamic 2D arrays are the gateway to matrices, image buffers, and grid-based simulations. The two-level allocation pattern is fiddly but mechanical. Get the cleanup right and the rest is bookkeeping. Pointers and memory module complete. Next: strings, where we spend 18 lessons mastering the standard library's string functions and their security implications.
Finish the lesson once you have worked through the material. This awards ★ 40 XP.