AlgoMaster Logo

Pointers & Arrays

High Priority14 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

C-style arrays and pointers in C++ are tangled together in a way that confuses almost everyone at first. In most expressions an array name turns into a pointer to its first element, which is why arr[i] and *(arr + i) mean the same thing, and why a function that "takes an array" is really taking a pointer. This chapter pulls that relationship apart for reading array code, writing functions that operate on arrays, and avoiding the classic "where did my array size go?" bug.

Array Names Decay to Pointers

When the name of a C-style array is used in most contexts, the compiler converts it into a pointer to the first element. This conversion is called array-to-pointer decay, or just array decay. The array still exists in memory the same way, but the expression arr evaluates to &arr[0], a pointer to the first slot.

Decay happening in plain sight:

Both p and q end up holding the same address: the address of the first element. The first assignment didn't need an explicit &prices[0] because the array name prices, used in a value context, already meant that.

This is the most important rule for this chapter: in most expressions, an array name is a pointer to its first element. It's not literally a pointer (a pointer variable has its own storage; the array doesn't), but it behaves like one when used.

arr[i] is *(arr + i)

Once an array name behaves like a pointer to its first element, indexing makes more sense. The expression arr[i] is defined to mean *(arr + i). The brackets are syntactic sugar over pointer arithmetic.

All four expressions read the same memory. stock + 2 advances by two int slots (eight bytes on most systems, but the arithmetic is in elements, not bytes), and dereferencing reads the value at that address. Once stock has decayed to a pointer, indexing through stock, indexing through p, and writing the dereferenced arithmetic all do the same job.

A consequence of this definition: since addition is commutative, arr + i and i + arr produce the same address, which means *(arr + i) and *(i + arr) are equivalent. The [] operator follows the same symmetry:

2[stock] is legal C++. The compiler rewrites it as *(2 + stock), which is the same as *(stock + 2). You would not write this in real code, but it's a clean demonstration that [] is sugar over pointer arithmetic, not a special array operator.

Where Decay Happens, and Where It Doesn't

Array decay isn't universal. It kicks in when the array name appears as a value in an expression, but a handful of contexts preserve the array type. Knowing the difference separates "reading array code" from "getting weird sizes back".

Decay does happen in:

  • Most expressions (arr + 1, *arr, arr == p, passing arr somewhere that expects a pointer).
  • Function arguments.
  • Initializing a pointer (int* p = arr;).

Decay does not happen in:

  • The operand of sizeof (so sizeof(arr) gives the byte size of the whole array, not the size of a pointer).
  • The operand of & (so &arr gives a pointer to the whole array, with a different type from a pointer to its first element).
  • When the array is bound to a reference parameter that preserves the size (void f(int (&arr)[5])).

A single program that shows all three "no decay" cases:

Three things are worth pulling out of that run. First, sizeof(stock) is 20, which is 5 * sizeof(int) on a typical 64-bit system. The compiler still knows the full type of stock inside sizeof, because sizeof is one of the operators where decay is suppressed. Dividing by sizeof(stock[0]) is the classic "how many elements does this array have?" idiom. Second, stock and &stock print the same address but have different types. stock decays to int*, while &stock has type int (*)[5], a pointer to an array of five ints. The numeric value is the same; the type is not. Third, inside byPointer the array has decayed, so sizeof(arr) reports the size of a pointer. Inside byReference the parameter type int (&arr)[5] preserves the array, so sizeof(arr) reports the full byte size.

The exact pointer size from sizeof(p) depends on the platform: 8 bytes on a 64-bit system, 4 on a 32-bit system. The numeric output for the addresses will differ across runs and machines.

sizeof is a compile-time operator. It produces a constant and never reads memory at runtime, so the idiom sizeof(arr) / sizeof(arr[0]) is free. The only thing it costs is correctness when arr has already decayed to a pointer.

Passing Arrays to Functions

Functions are where array decay becomes a daily concern. The moment void sumCart(double arr[]) is written, the compiler rewrites the parameter as double* arr. The function never receives the array as an array. It receives a pointer to the first element, and the size information is lost at the call site.

All three of these declarations are the same function:

The third form is the most explicit about what's happening, and it's the form most experienced C++ code uses. The first two look like they're carrying size information, but they aren't. A working example:

The size calculation happens in main, where cart is still a real array and sizeof(cart) gives the full byte count. The function signature takes a double* and an int count, the standard "pointer plus length" idiom. Inside the function, prices[i] works as it did on the original array, because indexing on a pointer is the same operation as indexing on an array (*(prices + i)).

The standard for loop form here is intentional. A range-based for loop would not work inside the function, because range-based for needs a real array or a container with begin() and end(). Once the array has decayed to a pointer, there's no way to know where the end is, which is why count has to be passed separately.

The Famous Pitfall: Lost Size Information

Getting the size wrong inside a function is a common array bug in C++. A broken version:

On a 64-bit system, sizeof(prices) is 8 and sizeof(prices[0]) is 8 (for a double), so count is 1. The loop sums only the first element and stops. The total is wrong, but the program compiles cleanly and runs without crashing, which is what makes the bug nasty. The compiler will usually issue a warning if built with -Wall, but a lot of legacy code lives on without that warning enabled.

The "size inside a function" bug doesn't cost CPU. It costs correctness. The function still runs at the same speed; it produces the wrong answer.

Better Options Than Raw Pointers

The pointer-plus-length idiom is the foundation, and it appears in millions of lines of C and C++ code. But modern C++ has cleaner options for new code:

A std::vector carries its own size, supports range-based for, and doesn't decay. Passing it by const& avoids a copy. For functions that need to operate on either a raw array or a vector or a std::array, C++20 added std::span, a non-owning view that bundles a pointer and a length together. That's beyond the scope of this chapter, but the option exists.

Prefer std::vector (or std::array for fixed sizes) for new code. Use the pointer-plus-length idiom when working with a raw array, an existing API, or a C interop boundary.

Memory Layout: An Array and a Pointer to It

All of this works because an array is a contiguous block of memory, and a pointer is an address. When prices decays to &prices[0], the resulting pointer points to the first element. Indexing with prices[i] or *(prices + i) walks forward through the same block, one element at a time.

The array itself, prices, lives in a single contiguous run of bytes. Each double takes 8 bytes on a typical system, so the addresses go up by 8 between neighboring elements. The pointer p, by contrast, has its own small slot of memory somewhere else (also on the stack here), and that slot holds the address 0x1000. Writing p + 1 doesn't change p; it produces a new address one element further on. Dereferencing that new address reads prices[1].

This is also why stock and &stock print the same numeric address but have different types. They start at the same byte. stock decays to a pointer to one int, while &stock is a pointer to "the whole block of five ints". They differ in what + 1 means: pointer arithmetic on int* moves by 4 bytes, pointer arithmetic on int (*)[5] moves by 20 bytes.

The addresses in the diagram are made up for illustration. Actual addresses will differ on every run because the stack layout is decided at runtime, but the relative offsets between elements are always the size of one element.

A Practical Pattern: Pointer Walk Over an Array

Once the decay rule is clear, a common pattern in C++ is to walk through an array using a pointer instead of an index. It's another spelling of the same loop, appearing in libraries, in legacy code, and in interview questions.

The pointer p starts at the first element and advances one element per iteration. The sentinel end is stock + count, a "one-past-the-end" pointer. C++ guarantees that computing this address is legal, but dereferencing it is undefined behavior. The standard library uses the same convention for iterators, which is why vec.end() is "past the last element", not "the last element".

Walking with a pointer and walking with an index compile to roughly the same machine code on modern compilers. Pick whichever reads more clearly. The pointer form is sometimes faster on older compilers and constrained platforms, but on a modern toolchain it's a style choice, not a performance one.

A Quick Word on Multidimensional Arrays

When a 2D array like int grid[3][4] decays, it doesn't become a int**. It becomes a pointer to an array of four ints: int (*)[4]. The first dimension is lost; the second is part of the element type and stays.

The unusual int (*)[4] syntax is part of the cost of using raw multidimensional arrays. Most production code uses std::vector<std::vector<int>>, a flat 1D vector with manual indexing, or std::array when sizes are known at compile time. The point for this chapter is that decay still happens for multidimensional arrays, one dimension at a time.

Common Misreadings

A few patterns are common sources of confusion. They're all consequences of the rules above, but they're worth calling out so they can be spotted in code reviews.

int arr[5] and int* p are not the same type, even though they often appear interchangeable. p = arr; works because arr decays. arr = p; doesn't work, because arr is not a modifiable variable. It's the name of a fixed block of memory.

sizeof(arr) inside a function never gives the array size. The "array" parameter is a pointer, full stop. To get the size, pass it. If the API can change, take a std::vector or std::span by reference.

arr[i] and *(arr + i) are not "almost" the same. They are defined to be the same. Compilers don't optimize one into the other; the language standard says they mean the same thing. That's also why negative indices like arr[-1] are syntactically legal: they compile to *(arr - 1), which may or may not point at a valid object. Don't write them unless arr is known to be offset into a larger buffer.

A function parameter written as int arr[] or int arr[100] is a pointer. The number is decoration. The compiler doesn't check it, doesn't enforce it, and doesn't preserve it. Some teams forbid the bracket form in parameters for this reason, requiring int* instead, so the truth is visible at the call site.

Quiz

Pointers & Arrays Quiz

10 quizzes