Skip to content

Rows and columns in two-dimensional arrays

Lesson 12 of 1322 minPDF notesFree

One array can contain smaller arrays

A one-dimensional array gives each element one index. Now suppose each row contains three integers and there are two rows. We still select one object at a time: first a row, then an integer within that row. Keeping those two selections separate makes the loops, pointer types and boundaries much easier to explain.

In this lesson you will read a two-dimensional initializer, trace a nested loop, place an accumulator reset correctly, scan a chosen column and distinguish movement between rows from movement between integers. You will explain the row type instead of treating every pointer as an interchangeable address.

Prerequisites: initialized arrays and valid indices; loop tests, updates and accumulators; pointer targets and copied values; array-to-pointer conversion; element-unit pointer arithmetic, one-past boundaries, sizeof, size_t and %zu. The earlier call model remains unchanged, but this lesson needs no new helper interface.

C11 is our teaching convention, not a version specified by the GATE syllabus. All actual arrays here have positive constant dimensions and initialized int elements. All int calculations remain within −32767 through 32767. Each complete program is independent. Explicitly marked classification fragments must never be compiled or executed. We assume no particular int byte size or pointer width. Variable-length arrays, allocation, general double-pointer programming and matrix algorithms are outside this lesson.

Read both dimensions and both initializer levels

For int grid[2][3] = {{4, 1, 7}, {2, 6, 3}};, read the declaration as an array of two elements, where each element is itself an array of three int objects. The outer array's element is a whole row. The row's element is one integer.

Row indexColumn 0Column 1Column 2
0417
1263

The first inner brace group initializes row 0, and the second initializes row 1. The dimensions are counts: two rows and three columns in every row. They are not the last valid indices.

  • grid[0] selects the first three-integer row
  • grid[1][2] selects column 2 within row 1, whose current value is 3
  • grid[0][1] selects a different integer, currently 1
  • A read or write of grid[r][c] requires both 0 <= r && r < 2 and 0 <= c && c < 3

The first index is not “whichever dimension is being scanned.” It always selects a row in this declaration. A column scan changes the row index while holding the column index fixed. Swapping the indices changes the selected object and can violate a different bound.

Brace grouping also makes omitted values clear. int ready[2][3] = {{5}, {2, 4}}; starts with rows {5, 0, 0} and {2, 4, 0}. The zeros come from the initializer rule, not from a general promise about automatic storage. An automatic declaration without an initializer does not acquire these guaranteed zeros. Our executable programs provide initialized elements before any read.

A nested loop finishes its inner work before advancing the outer index

A nested loop is a loop inside another loop's body. For a row scan, keep r fixed while c visits 0, 1 and 2. Only after that entire inner loop ends does the outer loop advance r.

Read the first worked program with this execution order:

  1. Initialize outer r to 0 and test r < 2
  2. Enter that row's outer body, initialize row_total to 0, and reach the inner loop
  3. Initialize inner c to 0, test c < 3, process the element, then update c
  4. Repeat the inner test/body/update until the test fails with c = 3
  5. Continue with the remaining statements in the outer body, including saving this row's total
  6. Finish the outer body, update r, and test the outer condition again

On the next successful outer test, the body reaches int row_total = 0; again. This starts the new row's total at zero. Reaching for (int c = 0; ...) again also starts that inner scan at column 0. Do not carry the previous inner scan's ending value 3 into the next row. Each declared c is local to its own execution of the inner loop; it is not available for use after that loop. These are source-level execution steps, not claims about machine instructions.

The reset point expresses the accumulator's purpose. Reset once per row to get separate row totals. Reset inside every inner body and you discard earlier values in the same row. Reset only before the outer loop and the value continues across rows. All three placements can stay within array bounds; only one meets the separate-row-total objective.

Worked example 1 Separate row totals and a whole-grid total

Predict the two row totals and the final combined total. The initialized one-dimensional totals array keeps the completed results; it is separate storage from grid.

C
#include <stdio.h>

int main(void)
{
    int grid[2][3] = {{4, 1, 7}, {2, 6, 3}};
    int totals[2] = {0, 0};
    int grand_total = 0;
    for (int r = 0; r < 2; r++) {
        int row_total = 0;
        for (int c = 0; c < 3; c++) {
            row_total += grid[r][c];
        }
        totals[r] = row_total;
        grand_total += row_total;
    }
    printf("%d %d | %d\n", totals[0], totals[1], grand_total);
    return 0;
}

The inner-body visit order and row totals are:

Visit numberrcValue readrow_total after addition
10044
20115
302712
41022
51168
612311

After row 0's inner loop, store 12 in totals[0] and add 12 to grand_total. After row 1's inner loop, store 11 in totals[1] and add 11 to the current 12. Output: 12 11 | 23, followed by a newline. grid is unchanged.

At the checkpoint just before an inner test, r identifies the current row and row_total is the sum of that row's columns already visited, from 0 through c - 1. At c = 0, this set is empty and the total is zero. At the failed test with c = 3, it covers the whole row. At each outer-test checkpoint, grand_total is the sum of all completed earlier rows; totals stores their individual totals. The unprocessed entries of totals retain their initialized zeros. These meanings explain both resets: row_total restarts per row, whereas grand_total retains completed rows.

The outer loop has two bodies and three condition tests. Each of its bodies runs an inner loop with three bodies and four condition tests. Altogether there are six inner bodies and eight inner condition tests, including a separate final failed test for each row. Both indices advance by one toward their finite bounds. No body runs at row 2 or column 3.

A second row total of 23 would carry the first row's total into the second. A first row total of 7 would discard earlier entries in that row. An order starting (0,0), (1,1) would incorrectly advance both indices together: the actual inner loop finishes all three columns before the outer update.

Worked example 2 Fix a column, then read before a selected update

This time only column 1 is selected. Add each visited integer's old value to before_total, and increase it by 4 only when it is below 5. The chosen column is within 0 through 2, and the loop visits the two existing rows. The shown small additions are representable in int.

C
#include <stdio.h>

int main(void)
{
    int grid[2][3] = {{3, 8, 1}, {6, 2, 5}};
    int column = 1;
    int before_total = 0;
    int changed = 0;
    for (int r = 0; r < 2; r++) {
        before_total += grid[r][column];
        if (grid[r][column] < 5) {
            grid[r][column] += 4;
            changed++;
        }
    }
    printf("%d %d | %d %d\n",
           before_total, changed, grid[0][column], grid[1][column]);
    return 0;
}
Visited rowFixed columnOld valuebefore_total after additionValue after possible storechanged
018880
1121061

At row 0, add 8. The condition 8 < 5 is false, so that element stays 8. At row 1, add the old 2, making before_total = 10; the condition is true, so store 6 and increase changed to 1. The array ends with rows {3, 8, 1} and {6, 6, 5}. Output: 10 1 | 8 6, followed by a newline.

Before each loop test, before_total contains the original values from the already visited rows of this selected column. changed counts how many of those positions received a store. Each selected position is visited once; other columns have not been read or changed by this loop. After the failed test at r = 2, these statements cover the whole chosen column. There are two bodies and three loop tests.

The new column total is 14, but before_total remains 10. A saved integer total does not automatically follow later element changes. Predicting 14 for that variable reverses the stated read/store order. Predicting three visits uses the column count as a row bound. No nested loop is needed for one fixed column: only one index varies.

Layout describes storage; loop order describes visits

The six integers in a 2-by-3 array are laid out in this order: grid[0][0], grid[0][1], grid[0][2], grid[1][0], grid[1][1], grid[1][2]. This is row-major layout. Each row is contiguous, and the two rows are consecutive elements of the outer array. The right-hand index advances through a row before the next row begins.

If one int occupies S C bytes, one row occupies 3 * S, and the whole grid occupies 2 * 3 * S. This is a symbolic size description, not executable pointer arithmetic or a claim that S is 4. It also does not authorize treating the inner arrays as a single six-element int array through a scalar pointer.

A program can choose a column-first visit order while storage remains row-major. A column-first scan would visit (0,0), (1,0), (0,1), (1,1), (0,2), (1,2). Interchanging loop nesting changes the sequence of accesses, not the declared shape or layout. Each access must still satisfy its own row and column bounds. We make no hardware timing claim from these small traces.

A pointer to a row is different from a pointer to an integer

Use the known shape int grid[2][3]. The declaration int (*row)[3] = grid; creates a pointer to an array of three int objects. The parentheses around *row matter: this declaration says that following row gives a three-integer array. We need only this one new pointer declaration, not general complicated declarators.

In this initializer, grid converts to a pointer to its first element. Its first element is the row grid[0], so the result has type int (*)[3]. It does not skip directly to an int * or turn into an int **.

By contrast, int *cell = grid[0]; selects row 0 and then converts that row expression to a pointer to its first integer, grid[0][0]. The conversion has happened at a different array level.

Expression in this stated contextType or selected object
Actual object gridArray of two arrays of three int objects
grid as initializer for rowint (*)[3], pointing to row 0
grid[0] as initializer for cellint *, pointing to row 0, column 0
&grid[0]int (*)[3], pointer to the first whole row
&grid[0][0]int *, pointer to the first integer
grid[1][2]The int object at row 1, column 2

Initially, with row = grid, row + 1 points to row 1. One step counts one pointed-to object, which here is an entire three-integer row. With cell = grid[0], cell + 1 points to row 0, column 1. Its step counts one int within that inner array. Neither expression alone changes its pointer variable or any stored integer. An assignment such as row = row + 1; saves the new pointer position.

Following row gives the selected row; (*row)[c] then selects integer c in that row. With row initially at row 0, row[1][2] selects row 1, column 2. After moving row to row 1, use (*row)[2] or row[0][2] for its column 2. Pointer-relative subscripts are relative to the current pointer position, just as in the previous lesson.

The two layers also have different one-past positions. grid + 2 is the valid endpoint after the two-row outer array, with no row there to access. grid[0] + 3 is the valid endpoint after the three integers of row 0, with no integer there to read or write through that pointer. To reach the next row's first integer, select it through grid[1][0] or a valid row pointer. Do not dereference the scalar endpoint to cross that inner-array boundary. In particular, reading grid[0][3] is invalid even though another row follows. Contiguous placement does not cancel the subarray's bound.

Finally, int ** means pointer to a pointer-to-int object. This grid stores rows of integers; it does not store an array of int * objects for a double pointer to follow. The converted row-pointer type int (*)[3] and int ** are incompatible. Do not substitute one for the other or add a cast to hide that mismatch. Recognizing the mismatch is the full double-pointer scope of this lesson.

Count rows and columns where the actual array is available

At the actual grid declaration, sizeof grid / sizeof grid[0] is 2: whole-grid size divided by one-row size. sizeof grid[0] / sizeof grid[0][0] is 3: one-row size divided by one-integer size. The array operands of these sizeof expressions keep their array types instead of converting to pointers.

Below we store both counts in size_t variables, as introduced in the previous lesson; use %zu to print those variables. A pointer variable called row is still a pointer, so sizeof row does not give the grid's total size or recover its number of rows. The fixed three-column row type determines the kind of step; it does not store a runtime count of available rows. We use no two-dimensional function parameters in this lesson.

Practice before opening the solutions

These four original transfer tasks are unscored. Give the reasoning checkpoint, selected indices or pointer targets as well as the numeric answer. The text-fenced material in Practices 3 and 4 is for classification only; never compile or execute it.

Practice 1 A row total and a column total

Give the output and the two sequences of visited positions. Which position participates in both calculations? Does that make it count twice within either individual calculation?

C
#include <stdio.h>

int main(void)
{
    int grid[2][3] = {{5, 2, 9}, {4, 7, 1}};
    int row_total = 0;
    for (int c = 0; c < 3; c++) {
        row_total += grid[1][c];
    }
    int column_total = 0;
    for (int r = 0; r < 2; r++) {
        column_total += grid[r][2];
    }
    printf("%d %d\n", row_total, column_total);
    return 0;
}

Practice 2 Diagnose a misplaced reset in a column-first scan

The intention is three separate column totals. This program stays within bounds, but does it meet that intention? Give its actual output, exact inner-body visit order and the counts of outer and inner loop-condition tests. Then move the initialization of total to the correct place and give the repaired output.

C
#include <stdio.h>

int main(void)
{
    int grid[2][3] = {{2, 4, 1}, {6, 3, 5}};
    int totals[3] = {0, 0, 0};
    int total = 0;
    for (int c = 0; c < 3; c++) {
        for (int r = 0; r < 2; r++) {
            total += grid[r][c];
        }
        totals[c] = total;
    }
    printf("%d %d %d\n", totals[0], totals[1], totals[2]);
    return 0;
}

Practice 3 Repair the varying dimension's bound

Read this unsafe fragment only. The intention is the sum of column 2. Identify the first out-of-bounds pair it would attempt. Repair only the loop bound, then give the repaired total and explain why replacing the column by 1 would not repair the row bound.

int grid[2][3] = {{1, 3, 2}, {7, 4, 6}};
int column = 2;
int total = 0;
for (int r = 0; r < 3; r++) {
    total += grid[r][column];
}

Practice 4 Keep the row pointer and scalar pointer separate

Give the full output and the final targets of row and cell. Explain both size ratios and each pointer step.

C
#include <stddef.h>
#include <stdio.h>

int main(void)
{
    int grid[2][3] = {{5, 1, 4}, {2, 8, 6}};
    int (*row)[3] = grid;
    int *cell = grid[0] + 1;
    row = row + 1;
    (*row)[0] = *cell + 3;
    cell = cell + 1;
    *cell = (*row)[1] - 2;
    size_t rows = sizeof grid / sizeof grid[0];
    size_t columns = sizeof grid[0] / sizeof grid[0][0];
    printf("%zu %zu | %d %d %d | %d %d %d\n",
           rows, columns, grid[0][0], grid[0][1], grid[0][2],
           grid[1][0], grid[1][1], grid[1][2]);
    return 0;
}

Separately classify each of the following in the context of that live 2-by-3 grid. They are independent alternatives, not subsequent statements. Distinguish a valid endpoint, an invalid element read and an incompatible declaration.

grid + 2
grid[0] + 3
grid[0][3]
grid[1][3]
int **wrong = grid;

Full solutions and wrong-turn feedback

Practice 1 solution

The first loop fixes row 1. It visits (1,0), (1,1), (1,2), reading 4, 7 and 1. Its running totals are 4, 11 and 12. At each test, row_total describes the visited prefix of row 1. The second loop fixes column 2, visiting (0,2), (1,2) and reading 9 then 1. Its independent running totals are 9 and 10.

Output: 12 10, followed by a newline. The grid is unchanged. Position (1,2) participates once in each calculation; it is not visited twice by either loop. The accumulators answer two different questions and do not share their running state.

A first total of 10 would calculate a column instead of the requested row. A second total of 22 would start with the first loop's completed 12 instead of the new zero. Each loop's varying index must use its own dimension's bound: three columns for the first loop, two rows for the second.

Practice 2 solution

The visits are (0,0), (1,0), (0,1), (1,1), (0,2), (1,2). The values read are 2, 6, 4, 3, 1 and 5. Without a per-column reset, the running totals are 2, 8, 12, 15, 16 and 21. The completed columns therefore store 8, 15 and 21. Actual output: 8 15 21, followed by a newline.

The outer loop runs three bodies and four tests. Each inner loop runs two bodies and three tests, so there are six inner bodies and nine inner tests overall. Reaching the inner declaration restarts r at 0 for each new column. The failed inner test at r = 2 does not end the outer loop.

Move int total = 0; inside the outer body, before the inner loop:

C
#include <stdio.h>

int main(void)
{
    int grid[2][3] = {{2, 4, 1}, {6, 3, 5}};
    int totals[3] = {0, 0, 0};
    for (int c = 0; c < 3; c++) {
        int total = 0;
        for (int r = 0; r < 2; r++) {
            total += grid[r][c];
        }
        totals[c] = total;
    }
    printf("%d %d %d\n", totals[0], totals[1], totals[2]);
    return 0;
}

Now each inner-test checkpoint says: total is the sum of the already visited rows of the current column only. Column 0 has totals 2 then 8; column 1 starts afresh, giving 4 then 7; column 2 starts afresh, giving 1 then 6. Repaired output: 8 7 6, followed by a newline. Visit order, loop-test counts and grid values are unchanged.

Resetting inside the inner body would instead keep only each column's last value, producing 6, 3 and 5. Swapping the loop bounds without changing what the indices select could create invalid accesses. The original issue is accumulator lifetime and reset placement, not layout or a pointer conversion.

Practice 3 solution

The valid row indices are 0 and 1, while the chosen column 2 is valid in each row. The proposed loop would first attempt an invalid access at (2,2): a third row does not exist. It has undefined behavior, so C11 prescribes no numeric output or guaranteed partial total for the original fragment. Moving to column 1 still leaves a nonexistent row 2.

Use r < 2. The repaired complete program is:

C
#include <stdio.h>

int main(void)
{
    int grid[2][3] = {{1, 3, 2}, {7, 4, 6}};
    int column = 2;
    int total = 0;
    for (int r = 0; r < 2; r++) {
        total += grid[r][column];
    }
    printf("%d\n", total);
    return 0;
}

It reads 2 and 6, with running totals 2 and 8. Output: 8, followed by a newline. It has two bodies and three tests; the failed test at row 2 prevents an element access there. The fix meets the full chosen-column objective. A bound of 1 would avoid the invalid access but would omit the second row. A bound of 3 came from the wrong dimension, not from a lack of a third integer in each existing row.

Practice 4 solution

Initially row points to row 0, while cell points to row 0, column 1. Moving row by 1 selects the next whole row, row 1, and leaves cell unchanged. The store (*row)[0] = *cell + 3 reads the 1 in row 0, column 1, and writes 4 into row 1, column 0.

Moving cell by 1 stays within row 0 and selects column 2. Then (*row)[1] reads row 1, column 1, whose value is 8. The subtraction gives 6, which is stored through cell into row 0, column 2. Final rows: {5, 1, 6} and {4, 8, 6}. The final pointer targets are the whole row 1 for row and row 0, column 2 for cell.

The first size ratio divides two-row storage by one-row storage and yields 2. The second divides a three-integer row by one integer and yields 3. Neither requires a numeric byte size. Both counts are stored in size_t variables, so their print formats are %zu; the six element values are int, printed with %d. Output: 2 3 | 5 1 6 | 4 8 6, followed by a newline.

Independent classifications:

  • grid + 2: valid one-past pointer for the outer array of rows; no row exists there to access
  • grid[0] + 3: valid one-past pointer for row 0's integer array; no integer is readable or writable through this endpoint
  • grid[0][3]: invalid integer read beyond row 0; the next row's contiguous storage does not make column 3 valid
  • grid[1][3]: invalid integer read beyond row 1; there is no fourth integer in that row either
  • int **wrong = grid;: incompatible pointer initialization that violates a C constraint and requires a diagnostic. grid supplies int (*)[3], not int **. This is not a valid C11 declaration to run for an output

Predicting that the first store changes row 0 ignores the row-sized step. Predicting that moving cell also moves row confuses independent pointer objects. Treating the two invalid column reads as part of a six-integer flat scan discards the inner-array bounds. Keep the type, current target and governing array together.

Before moving on

Trace a nested loop without advancing both indices together. Explain the chosen reset point, the two access bounds and the type that determines each pointer step. You are ready for the integrated learning review when you can combine those explanations with the earlier lessons' copied values, caller-visible stores and one-past reasoning. The review adds no new teaching topic. This module remains introductory array and pointer reasoning, not complete C or the complete GATE CS syllabus.

Semantic fact checks: WG14 N1570, clauses 5.1.1.3p1, 6.2.5p20, 6.3.2.1p3, 6.5.2.1p2–3, 6.5.3.2p3–4, 6.5.3.4, 6.5.6p8, 6.5.16.1p1, 6.7.6.2, 6.7.9p11, p17–21, 6.8p3, 6.8.5.3p1, 7.19 and 7.21.6.1. All teaching explanations, examples, traces and practice are original. This attribution does not certify historical source accounting or global rights clearance.

Quick reference

  • int grid[2][3] is an array of two rows; each row is an array of three int objects
  • grid[r][c] selects a row, then an integer; check both dimension bounds separately
  • In a row-outer nested loop, finish the inner column loop before the next outer row update
  • Reaching an inner loop again restarts its initializer; reset a separate total once per row or column, as intended
  • State an accumulator's meaning at a precise checkpoint, including whether it contains old or updated values
  • A fixed-column scan varies the row index and uses the row count as its bound
  • Row-major storage does not force row-first visits or permit scalar-pointer reads across inner-array boundaries
  • grid converts to int (*)[3]; a selected row such as grid[0] can convert to int *; neither makes this grid an int **
  • One row-pointer step selects the next row; one scalar-pointer step selects the next integer within its own row
  • At the actual array, whole size / row size counts rows, and row size / integer size counts columns; use size_t and %zu

Notes for this lesson

Sign in to keep your progress. Sign in