Course outline · 0% complete

0/27 lessons0%

Course overview →

Arrays, Pointer Arithmetic, and References vs Pointers

lesson 5-2 · ~12 min · 14/27

Raw arrays are how memory actually stores sequences: elements at consecutive addresses, nothing else. std::vector, std::string, and every cache-friendly structure you will ever use are built on exactly this layout, and interviewers probe it to check you know what your abstractions cost underneath.

C arrays and pointer arithmetic

A raw C array is a fixed-length block of elements sitting side by side in memory:

int a[4] = {10, 20, 30, 40};

The array's name converts to a pointer to its first element, and adding to a pointer moves it forward by whole elements (the compiler multiplies by the element size for you):

int* p = a;        // points at a[0]
std::cout << *p;        // 10
std::cout << *(p + 2);  // 30, two ints further along
p++;                    // now points at a[1]

In fact a[i] is defined as *(a + i). Indexing is pointer arithmetic. There is no bounds checking: a[100] compiles and reads whatever bytes happen to be there. Undefined behavior again.

You will mostly use std::vector (unit 8) instead of raw arrays, but interviews and real codebases expect you to understand this layer.

10203040a[0]a[1]a[2]a[3]p
p++ steps the pointer one whole element to the right. The elements of an array sit contiguously in memory, which is what makes this walk possible.

Walking a pointer across an array

Each iteration dereferences p to read a value, then advances it one element.

#include <iostream>

int main() {
    int a[4] = {10, 20, 30, 40};
    int* p = a;   // points at a[0]

    for (int i = 0; i < 4; i++) {
        std::cout << *p << "\n";
        p++;      // step to the next element
    }
    return 0;
}

Output

10
20
30
40

The initialization int* p = a; needs no &, unlike int* p = &x; for a plain variable. An array's name already converts to the address of its first element, so writing &a[0] would be equivalent but longer.

What p++ adds is 4 bytes rather than 1, because p is an int* and the compiler scales the step by the element size. That scaling is invisible and it is why the same p++ on a char* would move a single byte.

By the end of the loop p points one past the last element, which is a legal address to hold but not to dereference. That distinction is what the next example's loop condition relies on.

Summing an array with no indexing

A loop whose counter is the pointer itself. There is no i and no a[i] anywhere.

#include <iostream>

int main() {
    int a[5] = {2, 4, 6, 8, 10};
    int sum = 0;
    for (int* p = a; p != a + 5; p++) {
        sum += *p;
    }
    std::cout << sum << "\n";
    return 0;
}

Output

30

p holds an address and *p is the int living there, so sum += *p; accumulates values rather than addresses. Forgetting the star would be a compile error here, which is a small mercy.

The bound a + 5 is the address one past the last element, and C++ guarantees that this address is valid to compute and compare against even though dereferencing it is undefined behavior. That one-past-the-end marker is the standard shape for a range in C++, and it is exactly the begin/end iterator pair that unit 8 introduces for std::vector.

Note that the condition is != rather than <. Both work for a contiguous array, but != is the form that generalizes to iterators over linked structures, where no ordering comparison is available.

References vs pointers, the final scorecard

A reference (int&) is a permanent alias. A pointer (int*) is a variable holding an address. Both let one piece of code touch another's data.

reference int&pointer int*
can be nullno, must bind at creationyes, nullptr
can re-target laterno, bound onceyes, assign a new address
syntax to usejust the name*p to dereference
arithmeticnonep++, p + i

Rule of thumb in modern C++: use references when you can, pointers when you must (optional "might not exist" values, dynamic memory, walking arrays, linked structures). That is why lesson 4-2's function parameters used references: the callee always had something real to bind to.

What is true of references but not pointers

A reference must be bound to a real variable when it is created, and it can never be null. That is the property pointers do not share.

The binding also happens exactly once. A reference stays attached to that one variable for its whole life, so there is no reseating either, and an assignment through a reference writes to the target rather than rebinding the alias:

int x = 1, y = 2;
int& r = x;   // r is now permanently an alias for x
r = y;        // this sets x to 2. It does NOT make r refer to y.

int* p = &x;
p = &y;       // this really does repoint p at y

Pointers can be null, can be redirected, and support arithmetic, which is precisely why they are both more flexible and more dangerous. A reference parameter needs no null check, while a pointer parameter always raises the question of whether the caller might pass nullptr.

The same sum with an external counter

A variant that keeps a separate loop counter and uses the pointer purely for access, which is a common shape in older C-style code.

#include <iostream>

int main() {
    int a[4] = {10, 20, 30, 40};
    int* p = a;
    int sum = 0;

    for (int i = 0; i < 4; i++) {
        sum += *p;
        p++;
    }

    std::cout << "sum = " << sum << "\n";
    return 0;
}

Output

sum = 100

The two statements in the body can be collapsed into sum += *p++;, which dereferences the current position and then advances the pointer. That works because postfix ++ yields the old value, and while it is idiomatic C, spelling both steps out is easier to read.

This version has a weakness worth naming. The count 4 appears in the loop condition and nowhere near the array declaration, so changing the array to five elements silently leaves the sum wrong. The previous example's p != a + 5 has the same problem, and unit 8's range-based for over a std::vector is the fix for both, since the container carries its own size.