πŸ‡―πŸ‡΅ ζ—₯本θͺž | πŸ‡ΊπŸ‡Έ English
Advertisement

Multi-dimensional Arrays in C

Row-major layout, passing them to functions, dynamic allocation, matrix & image examples. Follows the basic arrays lesson.

πŸ’ͺ Not getting it first time is normal
The four ways to pass a multi-dim array confuse everyone. Learn just Pattern A (contiguous block) first and you'll be fine.
How to retry
  1. Refresh 1D arrays and pointers
  2. Burn in the "row-major" memory picture
  3. Pick one way to pass and stick with it (flat array + cols is easiest)
  4. Defer performance tuning β€” correctness first
πŸ’‘ Tip: int a[3][4] is really 12 contiguous ints. "2D" is just how you think about it.

2D & 3D declaration / init

2D array

// 3 rows Γ— 4 columns
int m[3][4] = {
    { 1,  2,  3,  4},
    { 5,  6,  7,  8},
    { 9, 10, 11, 12}
};

printf("%d\n", m[1][2]);  // β†’ 7 (row 1, column 2, 0-indexed)

3D array

// 2 layers Γ— 3 rows Γ— 4 columns
int cube[2][3][4];

for (int l = 0; l < 2; l++)
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 4; j++)
            cube[l][i][j] = l * 100 + i * 10 + j;
Shorthand inits: int m[3][4] = {0}; zero-fills. int m[][4] = {{1,2,3,4},{5,6,7,8}}; β€” only the outer size may be omitted; column counts are required.

sizeof layers

int m[3][4];
sizeof(m)        // 48  (3 * 4 * 4 bytes)
sizeof(m[0])     // 16  (one row = 4 * 4 bytes)
sizeof(m[0][0])  // 4   (one int)
// rows = sizeof(m)    / sizeof(m[0])
// cols = sizeof(m[0]) / sizeof(m[0][0])

Row-major memory layout

In C, 2D arrays are stored row by row in memory. This is called row-major order. Fortran and MATLAB use column-major β€” don't confuse them.

Logical view (3Γ—4)

1
2
3
4
5
6
7
8
9
10
11
12

Actual memory (12 contiguous ints)

1
2
3
4
5
6
7
8
9
10
11
12

Index formula

m[i][j] equals: *((int*)m + i * COLS + j)
or: int *p = (int *)m; p[i*COLS + j]
// Walk a 2D array as if it were 1D
int m[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int *p = (int *)m;
for (int k = 0; k < 12; k++)
    printf("%d ", p[k]);
// β†’ 1 2 3 4 5 6 7 8 9 10 11 12
Performance tip: in a double loop, keep the outer loop on rows, inner on columns. That matches row-major storage and is cache-friendly. Reversing the loops can be significantly slower on large arrays.

Four ways to pass to a function

Passing multi-dimensional arrays is one of C's most confusing corners. Here are the four options.

β‘  Fix the column count in the type (works since C89)

// column count is required; row count can be omitted
void print_34(int m[][4], int rows) {
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < 4; j++)
            printf("%d ", m[i][j]);
}

int a[3][4];
print_34(a, 3);
Pro: straightforward. Con: only accepts arrays with exactly 4 columns.

β‘‘ VLA parameter β€” C99+

// Declare rows/cols first, then take the VLA array
void print_any(int rows, int cols, int m[rows][cols]) {
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            printf("%d ", m[i][j]);
}

int a[3][4], b[2][5];
print_any(3, 4, a);
print_any(2, 5, b);
Pro: any size, keeps m[i][j] syntax. Con: requires C99+; old MSVC doesn't support VLAs.

β‘’ Flat array + manual indexing

// "Flatten" into 1D; compute the index yourself.
void print_flat(int *m, int rows, int cols) {
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            printf("%d ", m[i * cols + j]);
}

int a[3][4];
print_flat((int *)a, 3, 4);
Pro: works anywhere; pairs well with malloc. Con: you lose the m[i][j] syntax.

β‘£ Pointer-to-pointer (int **) β€” completely different!

// Pass an array of row pointers
void print_pp(int **m, int rows, int cols) {
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            printf("%d ", m[i][j]);
}

// caller must build an array of row pointers
int row0[] = {1,2,3};
int row1[] = {4,5,6};
int *rows[] = {row0, row1};
print_pp(rows, 2, 3);
Critical: casting int a[3][4] to int ** does NOT work. Their memory representations differ: one is a block of ints, the other is an array of pointers. This is the biggest source of confusion with multi-dim arrays.

Two dynamic-allocation patterns

For sizes determined at runtime, use malloc. The two common patterns:

Pattern A: contiguous block (preferred)

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

int main(void) {
    int rows = 3, cols = 4;

    // allocate all elements in one block
    int *m = malloc(sizeof(int) * rows * cols);
    if (!m) return 1;

    // manual indexing: m[i*cols + j]
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            m[i * cols + j] = i * 10 + j;

    free(m);
    return 0;
}
Advantages: one malloc call, no leaks, contiguous = cache-friendly.

Pattern A with VLA pointer for m[i][j] syntax

// Contiguous block AND 2D indexing
int (*m)[cols] = malloc(sizeof(int[rows][cols]));
m[1][2] = 42;   // normal subscripts work
free(m);

Pattern B: array of row pointers (int **)

int **m = malloc(sizeof(int *) * rows);    // row pointer array
for (int i = 0; i < rows; i++)
    m[i] = malloc(sizeof(int) * cols);       // one row each

m[1][2] = 42;   // m[i][j] syntax works

// free in reverse order
for (int i = 0; i < rows; i++) free(m[i]);
free(m);
Disadvantages: rows+1 malloc calls (slower, fragments memory), rows are scattered in RAM (worse cache). Prefer Pattern A unless you really need the jagged structure.

Example: matrix product

Compute C = A Γ— B where A is mΓ—k and B is kΓ—n. C[i][j] = Ξ£ A[i][l] * B[l][j].
#include <stdio.h>

#define M 2
#define K 3
#define N 2

void matmul(int A[M][K], int B[K][N], int C[M][N]) {
    for (int i = 0; i < M; i++)
        for (int j = 0; j < N; j++) {
            int sum = 0;
            for (int l = 0; l < K; l++)
                sum += A[i][l] * B[l][j];
            C[i][j] = sum;
        }
}

int main(void) {
    int A[M][K] = {{1,2,3},{4,5,6}};
    int B[K][N] = {{7,8},{9,10},{11,12}};
    int C[M][N];
    matmul(A, B, C);

    for (int i = 0; i < M; i++) {
        for (int j = 0; j < N; j++)
            printf("%4d", C[i][j]);
        putchar('\n');
    }
    // expected:
    //   58  64
    //  139 154
}
Loop order matters: swapping to ilj order (accumulating over l in the innermost loop) accesses B row-wise and can be significantly faster on large matrices thanks to row-major caching.

Example: horizontal flip (grayscale)

Treat the image as a 2D array and mirror each row.
#include <stdio.h>

#define W 6
#define H 4

void flip_horizontal(unsigned char img[H][W]) {
    for (int i = 0; i < H; i++)
        for (int j = 0; j < W / 2; j++) {
            unsigned char t = img[i][j];
            img[i][j] = img[i][W - 1 - j];
            img[i][W - 1 - j] = t;
        }
}

int main(void) {
    unsigned char img[H][W] = {
        {10,20,30,40,50,60},
        {11,21,31,41,51,61},
        {12,22,32,42,52,62},
        {13,23,33,43,53,63},
    };
    flip_horizontal(img);
    for (int i = 0; i < H; i++) {
        for (int j = 0; j < W; j++) printf("%3d ", img[i][j]);
        putchar('\n');
    }
}
output: 60 50 40 30 20 10 61 51 41 31 21 11 62 52 42 32 22 12 63 53 43 33 23 13
Extension: for RGB use a 3D array unsigned char img[H][W][3]. For a vertical flip, swap whole rows with memcpy.

Challenges

Challenge 1: transpose
Write a function that computes the transpose int B[4][3] of int A[3][4].
Challenge 2: multiplication table
Fill int table[9][9] with (i+1)*(j+1) and print it.
Challenge 3: dynamic matrix add
Take rows/cols as command-line args, allocate two contiguous matrices (Pattern A), fill with random numbers, print their sum.
Challenge 4: vertical flip
Rewrite the horizontal-flip example to flip vertically. Also write a version using memcpy to swap entire rows at once.
Challenge 5: trace
Implement int trace(int n, int m[n][n]) (VLA parameter) returning the sum of diagonal entries.

See also