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, move0 × sizeof(int)= 0 bytes forward. You're at the first element.numbers[3]means: start at the base address, move3 × 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:
- 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. - 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. - 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]isbase + 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.