Multidimensional Arrays in C: 2D Arrays, Row-Major Memory and Runnable Examples

Learn how C lays out and traverses fixed-size multidimensional arrays. A complete 3-by-4 sales program connects indexing, totals, addresses and function parameters.

KnowledgeGate Team

Exam prep & CS education

Updated 11 Aug 20267 min read

A one-dimensional array has one size and one subscript, but matrix[i][j] adds a row size, a column size and two loop boundaries that are easy to swap. Swap them and the code still compiles, then reads memory belonging to a different row. One 3-by-4 sales table is enough to fix the whole picture: row totals 44, 42 and 51, a grand total of 137, and the maximum 16 sitting at sales[2][1]. If one-dimensional arrays are still shaky, start with Arrays in C: Declaration, Initialization and Worked Examples. For the broader programming route, see Coding & DSA Courses for Placements.

Multidimensional arrays in C are arrays of arrays

Read int marks[2][3]; as 2 rows, each containing 3 int elements. Its 2-by-3 shape has 2 * 3 = 6 elements. Row indices are 0 and 1; column indices are 0, 1 and 2.

The general declaration is type name[ROWS][COLS]; access one element as name[row][column]. The first subscript selects a row, and the second selects an element inside it.

A 2D array can model a table, but C stores it as one contiguous array of fixed-size row arrays. It is not an int ** with separately allocated rows.

Declare, initialise and read a 2D array safely

Here is a declaration in which C infers the first extent:

int marks[][3] = {{72, 85, 91}, {68, 79, 88}};

C infers 2 rows. The inner extent must remain known because C needs the row width to locate the next one. Here, marks[0][1] is 85, marks[1][0] is 68, and marks[1][2] is 88.

The flat initializer int same[2][3] = {72, 85, 91, 68, 79, 88}; is equivalent, but row braces keep the shape visible.

Missing initializer values become zero. Thus int partial[2][3] = {{5, 6}, {9}}; produces [5, 6, 0] and [9, 0, 0]. An uninitialised automatic local array instead contains indeterminate values and must not be read.

Traverse a 3-by-4 sales table in a runnable C program

This program names both bounds and passes the table to a function that knows its row width.

#include <stdio.h>

enum { STORES = 3, MONTHS = 4 };

void summarise(size_t rows, const int sales[][MONTHS]) {
    int monthTotals[MONTHS] = {0};
    int grandTotal = 0;
    int maximum = sales[0][0];
    size_t maxStore = 0;
    size_t maxMonth = 0;

    for (size_t store = 0; store < rows; store++) {
        int storeTotal = 0;

        for (size_t month = 0; month < MONTHS; month++) {
            int value = sales[store][month];
            storeTotal += value;
            monthTotals[month] += value;

            if (value > maximum) {
                maximum = value;
                maxStore = store;
                maxMonth = month;
            }
        }

        grandTotal += storeTotal;
        printf("Store %zu total: %d\n", store, storeTotal);
    }

    printf("Month totals:");
    for (size_t month = 0; month < MONTHS; month++) {
        printf(" %d", monthTotals[month]);
    }
    printf("\nGrand total: %d\n", grandTotal);
    printf("Maximum: %d at [%zu][%zu]\n", maximum, maxStore, maxMonth);
}

int main(void) {
    int sales[STORES][MONTHS] = {
        {8, 12, 10, 14},
        {9, 7, 15, 11},
        {13, 16, 12, 10}
    };

    summarise(STORES, sales);
    return 0;
}

The rows total 8 + 12 + 10 + 14 = 44, 9 + 7 + 15 + 11 = 42, and 13 + 16 + 12 + 10 = 51. The columns total 8 + 9 + 13 = 30, 12 + 7 + 16 = 35, 10 + 15 + 12 = 37, and 14 + 11 + 10 = 35. Therefore, 44 + 42 + 51 = 137, with maximum 16 at sales[2][1].

The output is:

Store 0 total: 44
Store 1 total: 42
Store 2 total: 51
Month totals: 30 35 37 35
Grand total: 137
Maximum: 16 at [2][1]

The outer bound is the row count; the inner is the column count. Tying both to the shape protects non-square tables.

A 3-by-4 sales table with row totals 44, 42 and 51, column totals 30, 35, 37 and 35, grand total 137, and the maximum 16 highlighted.

Row-major memory layout explains every address

A built-in C multidimensional array stores one complete row before the next. For marks[2][3], the order is 72, 85, 91, 68, 79, 88.

For three columns, address of marks[i][j] = base + ((i * 3) + j) * sizeof(int). Assume a base of 1000 and 4-byte int for this example. The addresses are [0][0] at 1000, [0][1] at 1004, [0][2] at 1008, [1][0] at 1012, [1][1] at 1016 and [1][2] at 1020. Thus &marks[1][2] = 1000 + ((1 * 3) + 2) * 4 = 1020. The base and byte size are illustrative, not universal C guarantees.

A complete scan visits ROWS * COLS elements. The sales table makes 3 * 4 = 12 visits in O(ROWS * COLS) time. For the notation, revise Time Complexity and Asymptotic Notation: Big-O.

A row-major memory strip for int marks[2][3] showing six boxes from [0][0] at address 1000 to [1][2] at 1020, four bytes apart.

Pass a 2D array to a function without losing its row width

The program uses void summarise(size_t rows, const int sales[][MONTHS]), where MONTHS is 4. The equivalent pointer form is const int (*sales)[MONTHS]. The number of rows can arrive at runtime, but pointer arithmetic still needs the inner extent to find sales[i].

When passed to a function, int sales[3][4] becomes a pointer to an array of 4 integers, written int (*)[4]. It does not decay to int **. A function expecting int ** describes a different layout, so it is not a valid substitute. The decay rule and the sizeof traps that follow from it are worked separately in Arrays and Strings in C: Array-to-Pointer Decay, sizeof Traps and 2D Address Arithmetic.

Extend multidimensional indexing to three dimensions, then practise

int rgb[2][2][3] = {{{255, 0, 0}, {0, 255, 0}}, {{0, 0, 255}, {255, 255, 255}}}; models a 2-row by 2-column image with 3 colour channels. rgb[1][0][2] is 255, the blue channel at row 1, column 0. Traversal uses one loop per extent.

Try these before reading the answers:

  1. Transpose {{1, 4, 7}, {2, 5, 8}} from 2 by 3 using out[j][i] = in[i][j].

  2. Find both diagonal sums of {{2, 1, 0}, {3, 5, 4}, {7, 6, 9}}.

Exercise 1 gives the 3-by-2 array {{1, 2}, {4, 5}, {7, 8}}. In exercise 2, the primary diagonal is 2 + 5 + 9 = 16; the secondary is 0 + 5 + 7 = 12.

The transpose is easier to trust when it prints. This program fills out from in and prints the 3-by-2 result row by row:

#include <stdio.h>

int main(void) {
    int in[2][3] = {{1, 4, 7}, {2, 5, 8}};
    int out[3][2];

    for (size_t i = 0; i < 2; i++) {
        for (size_t j = 0; j < 3; j++) {
            out[j][i] = in[i][j];
        }
    }

    for (size_t row = 0; row < 3; row++) {
        printf("%d %d\n", out[row][0], out[row][1]);
    }

    return 0;
}

It prints:

1 2
4 5
7 8

Watch the shape change: in has 2 rows and 3 columns, while out has 3 rows and 2 columns, so the destination extents are the reverse of the source. Writing out[i][j] instead would reach out[0][2], which is outside a two-column row.

Common multidimensional-array errors and how questions expose them

Each common error has a direct correction:

  • for (i = 0; i <= ROWS; i++) goes one row beyond the array. Use i < ROWS.

  • Swapping ROWS and COLS breaks a non-square table. Match each index to its extent.

  • Passing the matrix as int ** misstates its type. Preserve the column width.

  • Reading an uninitialised local matrix is undefined behaviour. Initialise or assign every element first.

  • sizeof matrix / sizeof matrix[0] finds the row count only while matrix remains an array, not after parameter adjustment to a pointer.

For int a[2][3] = {{2, 4, 6}, {1, 3, 5}};, adding only when (i + j) % 2 == 0 selects a[0][0] = 2, a[0][2] = 6 and a[1][1] = 3. The result is 2 + 6 + 3 = 11.

If short grid[3][4] begins at 2000 and sizeof(short) is 2, then &grid[2][1] = 2000 + ((2 * 4) + 1) * 2 = 2018. Traces, bounds, addresses and parameter syntax all test consistent use of the shape.

The short version and next step

Write the shape, keep each index below its extent, use one loop per dimension, and preserve the column width in function parameters. These rules give row totals 44, 42, 51, grand total 137, and maximum 16 at [2][1].

For the sequenced language path from these basics through pointers and structures, continue with the C Language Course: Concepts, MCQs and Coding. Then change STORES to 4, add a fourth store row {6, 11, 9, 20}, and predict the new grand total 183 with the maximum 20 at [3][3] before you compile.