All Courses
C Intermediate

Arrays in C

Why Arrays Matter

Imagine you're writing a program to process the daily temperatures for an entire year. Without arrays, you'd need 365 separate variables: temp1, temp2, temp3... all the way to temp365. You'd need 365 lines of code just to declare them, and you couldn't loop over them. This is clearly unworkable. Arrays solve this problem by giving you a single name for a whole row of same-type values, stored back-to-back in memory, accessible by index.

Arrays are the most fundamental data structure in C. Every other data structure—strings, matrices, hash tables, heaps, you name it—is either built on top of arrays or uses them as a backing store. Operating systems use arrays for process tables. Graphics engines use arrays for vertex buffers. Database engines use arrays (called "pages") to store rows. Understanding arrays deeply means understanding how data is physically laid out in RAM, which is essential systems-programming knowledge.

1. Contiguous Memory Layout: The Secret to Speed

When you write int numbers[5];, the compiler doesn't scatter your five integers randomly across memory. Instead, it reserves one single, unbroken block of 5 × sizeof(int) bytes—typically 20 bytes on a system with 4-byte integers. All five elements sit right next to each other like houses on a street with no gaps between them.


  Memory layout for int numbers[5] = {10, 20, 30, 40, 50};
  (assuming int = 4 bytes, starting at address 0x1000)

  Address | Index | Value
  --------|-------|------
  0x1000  | [0]   | 10
  0x1004  | [1]   | 20
  0x1008  | [2]   | 30
  0x100C  | [3]   | 40
  0x1010  | [4]   | 50

This contiguous layout is what makes arrays fast. To reach any element, the CPU does one simple calculation: base_address + (index × element_size). This is a single multiply-and-add instruction. The CPU can also predict your access pattern and pre-fetch the next elements into its cache before you even ask for them—a phenomenon called spatial locality that makes sequential array access blazingly fast.

2. Zero-Based Indexing: It's an Offset, Not a Count

Why does numbers[0] give you the first element? Because the index is not a position number—it's an offset distance from the start. Let's break this down:

  • numbers[0] means: start at the base address, move 0 × sizeof(int) = 0 bytes forward. You're at the first element.
  • numbers[3] means: start at the base address, move 3 × sizeof(int) = 12 bytes forward. You're at the fourth element.

If arrays used 1-based indexing, the CPU would have to compute base + (index - 1) × size—a needless extra subtraction on every access. Zero-based indexing eliminates that subtraction, making array access as fast as physically possible.

3. Declaration, Initialization, and Access Patterns

C gives you several ways to create and fill arrays:

int arr1[5];                     // Uninitialized (garbage values!)
int arr2[5] = {10, 20, 30};      // arr2 = {10, 20, 30, 0, 0} — rest zero-filled
int arr3[] = {1, 2, 3, 4, 5};   // Size inferred as 5
int arr4[100] = {0};             // All 100 elements set to zero

Important: int arr4[100] = {0}; is the idiomatic way to zero-initialize an entire array. It works because the C standard guarantees that if you provide fewer initializers than the array size, the remaining elements are set to zero.

4. Traversing Arrays and Printing Addresses

Here's a complete program that demonstrates contiguous memory layout by printing both values and their addresses:

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int size = sizeof(arr) / sizeof(arr[0]);  // compute array length

    printf("Array size: %d elements
", size);
    for (int i = 0; i < size; i++) {
        // %p prints memory addresses in hexadecimal
        printf("Index %d: Value = %2d, Address = %p
",
               i, arr[i], (void*)&arr[i]);
    }

    // Notice how each address is exactly sizeof(int) = 4 bytes apart
    return 0;
}

Sample output (addresses will differ on your machine):

Array size: 5 elements
Index 0: Value = 10, Address = 0x7ffe12340000
Index 1: Value = 20, Address = 0x7ffe12340004
Index 2: Value = 30, Address = 0x7ffe12340008
Index 3: Value = 40, Address = 0x7ffe1234000c
Index 4: Value = 50, Address = 0x7ffe12340010

Notice the pattern: each address is exactly 4 greater than the previous one. This is the contiguous layout in action.

5. Multi-Dimensional Arrays (2D)

A 2D array represents a grid or matrix. In C, it's stored in row-major order: all elements of row 0 come first, then all of row 1, then row 2, etc. Even though we think of it as a table, in physical memory it's still one flat, contiguous sequence:

int matrix[2][3] = {
    {1, 2, 3},   // Row 0: occupies addresses 0x1000, 0x1004, 0x1008
    {4, 5, 6}    // Row 1: occupies addresses 0x100C, 0x1010, 0x1014
};
int val = matrix[1][2];  // Row 1, Column 2 = value 6

Memory layout for matrix[2][3]:


  Physical Memory Order:
  [1] [2] [3] [4] [5] [6]
   ^---Row 0---^ ^---Row 1---^

The formula for accessing matrix[row][col] is: base + (row × num_columns + col) × sizeof(element). For matrix[1][2]: base + (1 × 3 + 2) × 4 = base + 20, which is the 6th integer.

6. The DANGER: No Bounds Checking

Here is the single most dangerous fact about C arrays: the compiler performs zero bounds checking. If you declare int arr[5]; and then access arr[10], the compiler will happily compile your program without a single warning. At runtime, one of three things will happen:

  1. Silent data corruption: arr[10] happens to land on another variable's memory. Your program keeps running but produces wrong results. This is the worst outcome because you may not notice the bug for months.
  2. Segmentation Fault: arr[10] lands on protected memory. The OS kills your program immediately. This is actually good—you know something is wrong right away.
  3. Seems to work: arr[10] happens to land on unused memory. Your program runs fine... until you make a seemingly unrelated change, and then it mysteriously breaks. This is the most frustrating outcome.
// DANGEROUS CODE — DO NOT DO THIS
int data[5] = {1, 2, 3, 4, 5};
data[10] = 99;  // Compiles fine! Runtime behavior is UNDEFINED.
printf("%d
", data[10]);  // Might print 99, might crash, might print garbage

Lesson: You are the bounds checker. Always track your array sizes separately and validate indices before accessing.

7. Arrays and sizeof

The sizeof operator is your friend for working with arrays safely:

int arr[10];
printf("Total bytes: %zu
", sizeof(arr));           // 40 (10 * 4)
printf("Element size: %zu
", sizeof(arr[0]));       // 4
printf("Number of elements: %zu
", sizeof(arr) / sizeof(arr[0])); // 10

Critical warning: sizeof only works on the original array variable. Once you pass the array to a function (where it decays to a pointer), sizeof will return the pointer size (8 bytes), not the array size. This is one of the most common C bugs. We'll cover array decay in depth in the next lesson.

8. Common Mistakes

Mistake #1: Off-by-one errors

int arr[5] = {10, 20, 30, 40, 50};
for (int i = 0; i <= 5; i++) {  // BUG: accesses arr[5] which is out of bounds!
    printf("%d
", arr[i]);
}

The valid indices for an array of size N are 0 through N-1. Always use i < size, never i <= size.

Mistake #2: Forgetting to allocate space for the null terminator

char word[5] = "hello";  // BUG: needs 6 bytes for 'h','e','l','l','o',''

String literals include an invisible null terminator. "hello" is actually 6 characters. We'll cover strings in depth in a later lesson.

Mistake #3: Variable-length arrays (VLAs) on the stack

int n = 1000000;
int bigArray[n];  // DANGER: may overflow the stack (typically only 8 MB)

Large arrays should be allocated on the heap with malloc, not on the stack.

9. Key Takeaways

  • An array stores contiguous, same-type elements in memory. The address of arr[i] is base + i × sizeof(element).
  • Zero-based indexing exists because the index is an offset, not a position number. arr[0] is at offset 0 from the start.
  • Multi-dimensional arrays are stored in row-major order in one contiguous block of memory.
  • C performs no bounds checking. Accessing arr[10] on a 5-element array compiles fine and leads to undefined behavior at runtime. You must track sizes yourself.
  • Use sizeof(arr) / sizeof(arr[0]) to get the number of elements, but only on the original array variable—not after it has decayed to a pointer.

10. Practice Exercises

Exercise 1: Sum and Average

Write a program that declares an array of 5 integers, initializes them with values of your choice, then computes and prints both the sum and the average (as a float) of all elements. Use sizeof to make the loop independent of the array size.

Exercise 2: Reverse an Array In-Place

Write a function void reverse(int arr[], int size) that reverses the elements of an integer array in place (no second array allowed). For example, {1,2,3,4,5} becomes {5,4,3,2,1}. Test it from main.

Exercise 3: Find Maximum and Its Index

Write a function that takes an integer array and its size, finds the maximum value, and prints both the value and its index. If there are multiple occurrences of the maximum, print the first one. Test with arrays of different patterns (ascending, descending, random).

Exercise 4: 2D Matrix Trace

Write a program that declares a 3×3 integer matrix, initializes it with the numbers 1 through 9 in row-major order, and prints the trace (sum of the diagonal elements: positions [0][0], [1][1], [2][2]). The trace should be 1 + 5 + 9 = 15.