Multi-Dimensional Arrays in C Programming (2D, 3D Arrays Explained with Examples)
1. Introduction
If you are learning C programming, one topic you absolutely cannot skip is multidimensional arrays. From storing student grades in a table to building a game board, from image processing to matrix operations in engineering, multidimensional arrays are everywhere. Yet many beginners struggle with them. Why? Because understanding how they work in memory, how to traverse them efficiently, and how to pass them to functions requires a solid mental model — not just syntax memorisation.
This guide provides that foundation. By the end, you will understand:
- What multidimensional arrays are and how they work in memory.
- How to declare, initialize, and access 2D and 3D arrays.
- How C stores them using row-major order.
- How to pass them to functions correctly.
- How to allocate them dynamically.
- Real-world examples and common mistakes to avoid.
Let’s dive in.
Prerequisites
Basic knowledge of C variables, data types, and functions will help. This guide is self-contained, but some familiarity with code is required.
C Language Fundamentals: A Smart Way to Understand Variables, Constants, Keywords & More
Data Types in C Programming: int, float, char Explained
Learn Functions in C with Real-Life Coding Examples (2026 Guide)
Table of Contents
2. What is a multidimensional array?
A multidimensional array in C is an array that has more than one dimension. The simplest and most common form is the two-dimensional (2D) array, which you can think of as a table or matrix with rows and columns. Think of it this way:
- A 1D array is a single row of boxes.
- A 2D array is a grid of boxes (rows x columns).
- A 3D array is multiple grids stacked on top of each other (like pages in a book).
Multidimensional arrays are not a separate data structure — they are just arrays whose elements are themselves arrays. C handles them as one flat block of memory, which is the key to understanding their performance and behaviour.
3. Declaring a 2D array in C
The syntax for declaring a 2D array is:
data_type array_name[rows][columns];Examples
int matrix[3][4];
float grid[5][5];
char board[8][8];Here, matrix[3][4] means 3 rows and 4 columns, giving a total of 3 x 4 = 12 elements.
Important: All sizes must be known at compile time for a regular (static) array. For runtime sizes, you need dynamic allocation (covered in the upcoming section).
Initializing a 2D array
You can initialize a 2D array at the time of declaration in several ways.
Method 1 – With nested braces (recommended for clarity)
int marks[3][3] = {
{90, 85, 78}; // Row 0
{88, 92, 76}; // Row 1
{70, 65, 80} // Row 2
};Method 2 – In a single line
int marks[3][3] = {90, 85, 78, 88, 92, 76, 70, 65, 80};Both methods produce the same result. Method 1 is clearer and less error-free.
Method 3 – Partial initialization
int matrix[3][3] = {
{1, 2}, // Row 0: third element auto-set to 0
{4}, // Row 1: second and third auto-set to 0
{} // Row 2: all zeros
};C automatically fills unspecified elements with 0.
Method 4 – Zero-initialize everything
int grid[4][4] = {0}; // All 16 elements set to 04. Accessing Elements of a 2D array
Elements are accessed using two indices: array[row][column]. Both are zero-based.
int marks[3][3] = {
{90, 85, 78},
{88, 92, 76},
{70, 65, 80}
};
printf("%d\n", marks[0][0]); // 90 — Row 0, Column 0
printf("%d\n", marks[1][2]); // 76 — Row 1, Column 2
printf("%d\n", marks[2][1]); // 65 — Row 2, Column 1You can also assign values after declaration:
int grid[2][2];
grid[0][0] = 10;
grid[0][1] = 20;
grid[1][0] = 30;
grid[1][1] = 40;5. How C stores 2D arrays in memory (row-major order)
This is the most important concept to understand about multidimensional arrays in C. C stores 2D arrays in row-major order. This means all elements of row 0 are stored first, followed by all elements of row 1, and so on — in one flat, contiguous block of memory. There are no gaps.
Example:
For int grid[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
Memory layout (assuming base address 1000, int = 4 bytes):
Address Value Element
1000 1 grid[0][0]
1004 2 grid[0][1]
1008 3 grid[0][2]
1012 4 grid[1][0]
1016 5 grid[1][1]
1020 6 grid[1][2]
1024 7 grid[2][0]
1028 8 grid[2][1]
1032 9 grid[2][2]
The address formula
address of grid[i][j] = base + (i * num_cols + j) * sizeof(int)
Why does this matter?
Because of row-major order, always put the column index in the inner loop when traversing. This accesses memory sequentially, which is cache-friendly and significantly faster on large arrays.
// FAST — sequential memory access (cache-friendly)
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
process(grid[i][j]);
// SLOW — jumps through memory (cache-unfriendly)
for (int j = 0; j < cols; j++)
for (int i = 0; i < rows; i++)
process(grid[i][j]);6. Traversing a 2D array
The standard way to traverse a 2D array is with nested for loops.
Printing all elements
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int rows = 3, cols = 4;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%4d", matrix[i][j]);
}
printf("\n");
}
return 0;
}Output
1 2 3 4
5 6 7 8
9 10 11 12
Reading input into a 2D array
int grid[3][3];
printf("Enter 9 values:\n");
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
scanf("%d", &grid[i][j]);Finding the sum of all elements
int sum = 0;
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
sum += matrix[i][j];
printf("Sum = %d\n", sum);Finding the largest element
int max = matrix[0][0];
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
if (matrix[i][j] > max)
max = matrix[i][j];
printf("Max = %d\n", max);7. Passing a 2D Array to a Function
This is where many beginners get confused. When passing a 2D array to a function, you must specify the number of columns — the first dimension (rows) can be omitted, but all other dimensions are required. The compiler needs the column count to calculate memory offsets correctly.
Correct syntax
#include <stdio.h>
// Columns MUST be specified
void printMatrix(int arr[][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%4d", arr[i][j]);
}
printf("\n");
}
}
int main() {
int m[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
printMatrix(m, 3);
return 0;
}Alternative using a pointer to array
void printMatrix(int (*arr)[4], int rows) {
// Same behavior — arr is a pointer to a row of 4 ints
}Alternative using C99 Variable-Length Arrays (VLA)
void printMatrix(int rows, int cols, int arr[rows][cols]) {
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
printf("%4d", arr[i][j]);
}The VLA approach is the most flexible — it lets you pass any size without hardcoding the column count. It requires C99 or later (gcc -std=c99).
Key Rule
Always pass the number of rows and columns explicitly. Never rely on sizeof inside the function — by then, the array has decayed and sizeof gives the pointer size, not the array size.
8. 3D array in C
A 3D array adds a third dimension — you can think of it as multiple 2D matrices stacked together, like the pages of a book.
Declaration
int cube[layers][rows][cols];Example
int cube[2][3][3] = {
// Layer 0
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
},
// Layer 1
{
{10, 11, 12},
{13, 14, 15},
{16, 17, 18}
}
};
// Access: cube[layer][row][column]
printf("%d\n", cube[0][1][2]); // Output: 6
printf("%d\n", cube[1][2][0]); // Output: 16Traversing a 3D array
for (int l = 0; l < 2; l++) {
printf("Layer %d:\n", l);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
printf("%4d", cube[l][i][j]);
}
printf("\n");
}
printf("\n");
}Memory formula for 3D arrays
address of cube[l][i][j] = base + (l*R*C + i*C + j) * sizeof(type)
Where R = number of rows, C = number of columns.
Warning
3D arrays grow very fast. A
float data[100][100][100]array holds 1,000,000 floats = approximately 4 MB. This will cause a stack overflow as it is declared as a local variable. Use static or malloc for large arrays.
9. Dynamic multidimensional arrays
Static arrays require sizes known at compile time. When you need sizes determined at runtime, use dynamic allocation with malloc.
Method 1 – Array of pointers (flexible / jagged)
This approach allocates each row separately. Rows can even have different lengths (jagged arrays).
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 3, cols = 4;
// Step 1: Allocate an array of row pointers
int **matrix = (int **)malloc(rows * sizeof(int *));
// Step 2: Allocate each row
for (int i = 0; i < rows; i++)
matrix[i] = (int *)malloc(cols * sizeof(int));
// Use it exactly like a regular 2D array
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
matrix[i][j] = i * cols + j + 1;
// Print
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++)
printf("%4d", matrix[i][j]);
printf("\n");
}
// Step 3: Free each row, then the pointer array
for (int i = 0; i < rows; i++)
free(matrix[i]);
free(matrix);
return 0;
}Output
1 2 3 4
5 6 7 8
9 10 11 12
Method 2 – Single malloc (contiguous, faster)
Allocates all elements in one block. This is faster due to better cache performance and simpler to free.
int rows = 3, cols = 4;
// One block for everything
int *matrix = (int *)malloc(rows * cols * sizeof(int));
// Access element [i][j] with manual indexing
matrix[i * cols + j] = value;
// Free with a single call
free(matrix);Which method to choose?
| Method 1 (array of pointers) | Method 2 (single malloc) | |
| Syntax | matrix[i][j] | matrix[i * cols + j] |
| Memory | Non-contiguous | Contiguous |
| Cache performance | Slower | Faster |
| Jagged rows | Yes | No |
| Free calls | rows + 1 | 1 |
For performance-critical code (image processing, scientific computing), always prefer Method 2.
10. Real-world example – matrix multiplication
Let’s apply everything with a complete, practical example. Matrix multiplication is a foundational operation in linear algebra, graphics, and machine learning.
Formula
For matrices A(n * k) and B(k * m), result C(n * m):
C[i][j] = sum of A[i][p] * B[p][j] for p = 0 to k-1
#include <stdio.h>
#include <stdlib.h>
// Allocate an n×m matrix, initialized to 0
int **createMatrix(int n, int m) {
int **mat = (int **)malloc(n * sizeof(int *));
for (int i = 0; i < n; i++)
mat[i] = (int *)calloc(m, sizeof(int));
return mat;
}
// Free a matrix
void freeMatrix(int **mat, int n) {
for (int i = 0; i < n; i++)
free(mat[i]);
free(mat);
}
// Print a matrix
void printMatrix(int **mat, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++)
printf("%6d", mat[i][j]);
printf("\n");
}
}
// Multiply A(n×k) × B(k×m) → C(n×m)
int **multiply(int **A, int **B, int n, int k, int m) {
int **C = createMatrix(n, m);
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
for (int p = 0; p < k; p++)
C[i][j] += A[i][p] * B[p][j];
return C;
}
int main() {
// A = 2×3 matrix
int **A = createMatrix(2, 3);
int valA[6] = {1, 2, 3, 4, 5, 6};
for (int i = 0; i < 2; i++)
for (int j = 0; j < 3; j++)
A[i][j] = valA[i * 3 + j];
// B = 3×2 matrix
int **B = createMatrix(3, 2);
int valB[6] = {7, 8, 9, 10, 11, 12};
for (int i = 0; i < 3; i++)
for (int j = 0; j < 2; j++)
B[i][j] = valB[i * 2 + j];
printf("Matrix A:\n");
printMatrix(A, 2, 3);
printf("\nMatrix B:\n");
printMatrix(B, 3, 2);
int **C = multiply(A, B, 2, 3, 2);
printf("\nResult C = A × B:\n");
printMatrix(C, 2, 2);
freeMatrix(A, 2);
freeMatrix(B, 3);
freeMatrix(C, 2);
return 0;
}Output
Matrix A:
1 2 3
4 5 6
Matrix B:
7 8
9 10
11 12
Result C = A × B:
58 64
139 154
11. Common mistakes to avoid
Mistake 1 – off-by-one error
int arr[3][3];
arr[3][0] = 5; // WRONG — valid indices are 0, 1, 2Arrays are zero-indexed. arr[3] is out of bounds — undefined behaviour.
Mistake 2 – Wrong loop order (cache performance issue)
// SLOW — avoid this for large arrays
for (int j = 0; j < cols; j++)
for (int i = 0; i < rows; i++)
process(arr[i][j]);Always iterate rows in the outer loop and columns in the inner loop.
Mistake 3 – Forgetting to free dynamic arrays
int **matrix = allocate(3, 4);
// ... use matrix ...
// Missing: free each row, then the matrix itselfAlways free in reverse order of allocation. Use tools like Valgrind to detect memory leaks.
Mistake 4 – Omitting column size in function parameters
void process(int arr[][], int rows) { } // WRONG — compiler error
void process(int arr[][4], int rows) { } // CORRECTMistake 5 – Forgetting nested loops
Leads to incomplete access to data
Mistake 6 – Stack overflow with large arrays
int bigData[1000][1000][10]; // ~40 MB on stack — crash!Use static or malloc for large multidimensional arrays.
Mistake 7 – Using sizeof inside a function
void func(int arr[][4], int rows) {
int size = sizeof(arr); // Returns pointer size, NOT array size!
}Always pass dimensions explicitly as function parameters.
12. Conclusion
Multidimensional arrays are one of the most powerful and widely used features in C programming. Understanding them deeply — not just the syntax, but the memory model behind them — is what separates a good C programmer from a great one. Here is a quick recap of everything we covered:
- A 2D array in C is declared as
type name[rows][cols]and accessed withname[i][j]. - C stores all multidimensional arrays in row-major order — a single flat block of memory, row by row.
- Always iterate with the column index in the inner loop for cache-friendly performance.
- When passing a 2D array to a function, you must always specify the column size.
- Use malloc for dynamic 2D arrays; prefer a single allocation for performance-critical code.
- Always free dynamically allocated arrays when done, and always pass dimensions explicitly to functions
The best way to solidify this knowledge is to write code. Build a tic-tac-toe board, implement a grade tracker, or write your own matrix library. The more you use multidimensional arrays, the more natural they become.
Now that you understand how arrays work in memory, the next step is to understand:
- How pointers interact with arrays.
- Why arrays behave like pointers
- When to use each.
