Demystifying C Arrays: Why Elements Are Perfectly Packaged in Memory
Have you ever wondered what actually happens under the hood when you create an array in C?
Many high-level languages hide memory management from you. But C forces you to look directly at the machine. Today, we’re going to look at two simple programs, analyze their outputs, and decipher exactly how the computer arranges elements side-by-side in RAM.
Part 1: Proving Continuous Layout in Memory
Let’s start with a foundational example. This short program initializes an array of 5 integers, accesses them using standard indexing and pointer arithmetic, and dumps their physical addresses:
#include <stdio.h>
int main(void) {
// 1. Initialize an array of 5 integers
int arr[5] = {2, 4, 6, 8, 10};
// 2. Create a pointer pointing to the first element of the array
int *ptr = arr;
// 3. Access elements using standard array syntax
printf("Accessing with array index\n");
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
// 4. Access elements using pointer arithmetic
printf("\nAccessing with pointers\n");
for (int i = 0; i < 5; i++) {
printf("*(ptr + %d) = %d\n", i, *(ptr + i));
}
// 5. Output the literal hardware address of each element
printf("\nAccessing memory address:\n");
for (int i = 0; i < 5; i++) {
printf("&arr[%d] = %p\n", i, (void *)&arr[i]);
}
return 0;
}
Expected Output for Program 1
When you compile and run this program, you will see an output that looks like this:
Accessing with array index
arr[0] = 2
arr[1] = 4
arr[2] = 6
arr[3] = 8
arr[4] = 10
Accessing with pointers
*(ptr + 0) = 2
*(ptr + 1) = 4
*(ptr + 2) = 6
*(ptr + 3) = 8
*(ptr + 4) = 10
Accessing memory address:
&arr[0] = 0x7ffc6f81bed0
&arr[1] = 0x7ffc6f81bed4
&arr[2] = 0x7ffc6f81bed8
&arr[3] = 0x7ffc6f81bedc
&arr[4] = 0x7ffc6f81bee0
What’s Happening Here?
- Array Names as Addresses: The two loops printing values match perfectly. In C, the name of an array (
arr) acts directly as a reference to its very first element. That’s why we can assign it to our pointer (int *ptr = arr;) without using an ampersand (&). - Interchangeable Syntax: Standard array indexing (
arr[i]) is just syntactic sugar! Behind the scenes, the compiler automatically evaluates it as pointer arithmetic:*(arr + i).
Deciphering the Hexadecimal Addresses
At first glance, the memory addresses look intimidating. Let’s break them down using basic math.
1. The Hex Step (Base-16)
The 0x prefix warns us that the number is written in hexadecimal. Instead of counting from 0 to 9, hex counts from 0 to 15 using letters for double digits:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a (10), b (11), c (12), d (13), e (14), f (15).
Look closely at the very last character of each address from our output:
arr[0]ends in0arr[1]ends in4arr[2]ends in8arr[3]ends inc(which represents 12)
Because a standard integer typically occupies exactly 4 bytes of memory, each subsequent integer sits exactly 4 units higher than the previous one.
2. Handling the “Rollover”
Look at the transition from arr[3] to arr[4]:
&arr[3] = 0x7ffc6f81bedc&arr[4] = 0x7ffc6f81bee0
The last digit resets to 0, and the second-to-last character rolled over from d to e. Just like 19 rolls over to 20 in normal decimal math, d rolls over to e in hex when it runs out of single-digit room.
3. Converting to Decimal
If we isolate the changing tail-end of those last two addresses and convert them to standard base-10 numbers, the math becomes perfectly clear:
&arr[3] –> …dc = (13 * 16) + 12 = 220
&arr[4] –> … e0 = (14 * 16) + 0 = 224
Subtract the two total values: 224 - 220 = 4 (bytes).
Part 2: The Logic of Pointer Arithmetic
Now that we know arrays live sequentially in memory, how does C manage step-by-step traversal? Let’s introduce a second program that simplifies data tracking and demonstrates how pointers automatically gauge distance:
#include <stdio.h>
int main(void) {
// 1. Trace values and sequential addresses inline
int numbers[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
printf("numbers[%d]: %d\n", i, numbers[i]);
printf("address of numbers[%d]: %p\n", i, (void *)&numbers[i]);
}
// 2. Explore pointer stepping behavior
int an_array[3] = {2, 4, 6};
/* Each time you add 1 to the pointer, it moves forward by one element,
not one byte, but one object of that type.
*/
int *p = an_array; // Points directly to an_array[0]
printf("\nPointer stepping results:\n");
printf("%d, %d, %d\n", *p, *(p + 1), *(p + 2));
return 0;
}
Expected Output for Program 2
Running this program will output the following results:
numbers[0]: 1
address of numbers[0]: 0x7ffc6f81bed0
numbers[1]: 2
address of numbers[1]: 0x7ffc6f81bed4
numbers[2]: 3
address of numbers[2]: 0x7ffc6f81bed8
numbers[3]: 4
address of numbers[3]: 0x7ffc6f81bedc
numbers[4]: 5
address of numbers[4]: 0x7ffc6f81bee0
Pointer stepping results:
2, 4, 6
Explaining Program #2
The second half of this program highlights the most important rule of data navigation in C: Pointers are type-aware.
When you look at the expression *(p + 1), it looks like you are adding the literal integer 1 to the pointer. However, the compiler does not take that as adding “1 byte”.
Object Steps vs. Byte Steps
Because p is declared as an int * (a pointer to an integer), the compiler automatically handles the data layout math. It knows that every object in that array takes up 4 bytes of space. Therefore:
pholds the location of the first item (2).p + 1automatically shifts forward by 1 object ($1 \times 4\text{ bytes} = 4\text{ bytes}$), landing perfectly on the second item (4).p + 2shifts forward by 2 objects ($2 \times 4\text{ bytes} = 8\text{ bytes}$), landing perfectly on the third item (6).
If you were to change an_array to hold double variables (which are typically 8 bytes each) and updated p to be a double *, typing p + 1 would automatically shift your hardware destination by 8 bytes instead of 4.
Conclusion
By mapping out the raw data, we can prove mathematically that arrays are not random scatterings of information. C guarantees that your list items are stuffed into memory side-by-side in one continuous block.
When you ask for arr[3] or *(ptr + 3), the computer doesn’t search around. It performs a split-second calculation: $\text{Base Address} + (\text{Index } 3 \times \text{Size of Data Type})$. It leaps forward the correct number of bytes and drops you right onto your data. That is the efficiency, power, and beauty of layout control in C!