Pointers, Arrays, and Pointer Arithmetic
The Array as a Memory Block
Memory Visualization
In C++, an array is stored as a contiguous block of memory. This means elements are placed right next to each other in sequence.
Think of memory as a series of numbered boxes. When you declare int arr[3], C++ reserves space for three integers, starting at a specific memory address.
Welcome back. To master pointers, we first need to visualize how arrays live in your computer's RAM. Imagine memory as a series of numbered boxes. When we declare an array of three integers, C++ reserves a contiguous block of space. If the first box is at address zero-x-one-zero-zero, the next must be at zero-x-one-zero-four because an integer typically takes up four bytes.
- Arrays occupy contiguous memory slots.
- Each data type has a specific byte size (e.g., int = 4 bytes).
- Memory addresses are usually represented in hexadecimal (e.g., 0x100).
Memory as a Contiguous Block
The Mental Model
In C++, an array isn't just a collection; it's a contiguous block of memory. When you declare int arr[3], the compiler reserves three consecutive 'boxes' in RAM.
Because the memory is contiguous, the computer can calculate the location of any element if it knows the starting address and the size of the data type.
To master pointers, we must first visualize how arrays live in your computer's RAM. When we define an array of three integers, C++ grabs a solid chunk of memory. Notice how each index corresponds to a specific address, jumping by 4 bytes each time because that is the size of an integer. Each cell stores a value. By knowing the starting address, the CPU can instantly find any index using simple math.
- Arrays occupy contiguous memory blocks.
- Each element has a unique memory address.
- Addresses are usually represented in hexadecimal (e.g., 0x100).
The Secret of Array Decay
What is Array Decay?
In C++, the name of an array is not just a label—it acts as a pointer to its first element. This implicit conversion is called Array Decay.
int arr[3] = {10, 20, 30};
int* p = arr; // arr decays to &arr[0]
The most important rule to remember is Array Decay. In most contexts, the name of an array implicitly converts into a pointer to its first element. This means the variable 'arr' is essentially a synonym for the address of its first index. If you dereference it, you get the value stored right at the start.
- Array names decay into pointers in most contexts.
- 'arr' is essentially a synonym for '&arr[0]'.
- Dereferencing the array name (*arr) gives you the first value.
The Rule of Array Decay
What's in a Name?
One of the most critical concepts in C++ is Array Decay. In most expressions, the name of the array implicitly converts into a pointer to its first element.
int* p = arr; // Decay happens hereThis means arr and &arr[0] are effectively the same memory address.
You might wonder: what exactly is the variable 'arr' itself? In C++, the name of the array 'decays' into a pointer. It literally points to the very first box in that memory block we just saw. This means using 'arr' in code is usually just a shorthand for 'the address of the first element'.
- Array names 'decay' into pointers.
- arr is a synonym for &arr[0].
- Dereferencing the array name (*arr) gives the first element's value.
Pointer Arithmetic & Scaling
The Scaling Factor
When you add 1 to a pointer, you aren't adding 1 byte. You are adding one unit of the data type. The compiler automatically scales the addition by the sizeof() the type.
- int* + 1: +4 bytes
- char* + 1: +1 byte
- double* + 1: +8 bytes
Now, let's look at pointer arithmetic. When you add one to a pointer, you aren't moving forward by one single byte. Instead, the compiler uses a scaling factor. For an integer pointer, adding one moves the address forward by four bytes. But for a character pointer, it only moves by one byte. This ensures you always land exactly at the start of the next element. Notice how the integer pointer jumps from zero-x-one-zero-zero to zero-x-one-zero-four. The character pointer, however, only moves to zero-x-one-zero-one.
- Pointer arithmetic is type-aware.
- The compiler multiplies the offset by the byte-size of the type.
- arr[i] is just 'syntactic sugar' for *(arr + i).
Pointer Arithmetic & Scaling
The Scaling Factor
When you add 1 to a pointer, the compiler doesn't just add 1 byte. It adds sizeof(T) bytes, where T is the data type.
- int*: +1 moves 4 bytes
- char*: +1 moves 1 byte
- double*: +1 moves 8 bytes
Pointer arithmetic is smarter than basic math; it uses a scaling factor. If you increment an integer pointer, it leaps 4 bytes to reach the next integer. But a character pointer only moves 1 byte. This ensures that 'plus one' always means 'the next item', regardless of how big that item is.
- Pointer arithmetic is type-aware.
- The compiler handles the 'offset' math automatically.
- This allows us to navigate arrays safely regardless of data size.
Practice: Predict the Output
Code Challenge
Examine the code snippet. Based on the scaling rules we just discussed, what will the two print statements output?
Assume numbers starts at address 0x100.
Time for a challenge. Look at this snippet where we have both an integer pointer and a character pointer looking at the same array. Based on what you've learned about scaling, what will these lines print? Type your predictions and submit.
- Predicting offsets for different pointer types.
- Understanding dereferencing vs. address calculation.
Predict the Jump
Look at the code and predict the output. How does the data type change the result of pointer arithmetic?
It's time to test your intuition. Look at this snippet. We have an array of integers, but we're looking at it through two different lenses: an integer pointer and a character pointer. Type in what you think the printed values will be.
- Apply scaling factors to real code.
- Distinguish between value dereferencing and address math.
Iterating with Pointers
The Pointer Loop
We can navigate an array without using indices by using start and end pointers. This is a common pattern for high-performance C++ code.
int* ptr = arr;
int* end = arr + 3;
while(ptr < end) {
// do work
ptr++;
}
Instead of using an index like 'i', we can use pointers to walk through memory directly. We set one pointer to the start and another just past the end as a 'sentinel'. As we increment the pointer, it steps through the array until it hits the sentinel, providing a clean and efficient way to process data.
- Pointers can be compared (ptr < end).
- The 'end' pointer usually points one past the last element.
- ptr++ moves to the next memory address.
Iterating with Pointers
The Pointer Loop Pattern
Instead of using an index i, low-level C++ often uses pointers to traverse data. We use a start pointer and a sentinel (end) pointer.
ptrstarts atarr.endpoints just past the last element.- Loop while
ptr < end.
In professional systems programming, we often iterate using only pointers. We set a pointer to the start of the array and a sentinel pointer to the end. As we increment the pointer, it 'walks' through memory. The loop continues as long as our current address is less than the end address.
- Pointer comparison (ptr < end) is a valid loop condition.
- Incrementing (ptr++) moves the pointer to the next element automatically.
- Common pattern in systems programming.
The Socratic Pointer Tutor
Ask the tutor about pointer arithmetic or array decay to clarify your understanding.
Pointers are notoriously tricky. If you're still feeling a bit hazy about how adding 1 to a pointer works, or why arrays decay, ask me a question. I'll help guide you to the answer.
- Clarify confusing concepts.
- Deepen understanding of memory offsets.
Common Pitfalls
Danger Zones
Pointers are powerful but offer no safety rails. Two major traps await the unwary:
- Buffer Overflows: Accessing memory outside the array bounds.
- Size Loss: Forgetting that
sizeof(ptr)is just the pointer size (usually 8 bytes).
Before we wrap up, a word of caution. Pointers give you direct access to memory, but they don't come with safety rails. If you increment a pointer too far, you'll access memory you don't own, leading to crashes. Also, remember that once an array decays, it loses its size information. To the compiler, it's just a lonely address.
- C++ does not check array bounds at runtime.
- Losing array size information is called 'Pointer Sinking' or 'Decay'.
- Always track array sizes manually or use std::vector.