Concepts / Built-in Arrays and Pointer Decay

Built-in Arrays and Pointer Decay

The source gives the main reason for preferring array over a built-in array as the way array avoids surprising and dangerous conversions to pointers, which is a reason beyond size and copying alone.

  • Programming

Why Choose a Fixed-Size Container

Once you know vector, a natural question follows: if vector is more flexible, why would anyone choose array? The answer is not that array is more powerful. Array gives up flexibility in exchange for simplicity, and in some situations that trade is worth making. As you read, keep one question in mind: what does the program know about the container, and what can the program do with it? The answers differ between vector, array, and a built-in array, and most of the practical differences follow from those answers.

What do you think happens?

Does a built-in array carry its own length with it?

  • Yes

  • No

Reveal answer
Answer:

No. A built-in array does not carry its length, so code that receives one has to be told the length separately.

This missing length is one of the reasons array is preferred over a built-in array, and it is the thread that runs through the rest of this article.

Size, Copying, and Conversion Side by Side

The source names two practical advantages an array has over a built-in array. The first is that an array knows its size. The second is that an array can be copied using the assignment operator or through initialization, so it behaves like an ordinary value that can be passed around and duplicated. A built-in array does not support copying in that same way, and it does not carry its length. The table below lines up the three containers on the questions that matter most in practice.

QuestionBuilt-in arrayArrayVector
Does it know its own size?No, length must be passed separatelyYesYes
Can it be copied with assignment or initialization?Not in the same wayYesYes
Does it convert to a raw pointer on its own?Yes, it decays to a pointer to its first elementThe protection against the pointer conversion hazard appliesThe protection against the pointer conversion hazard applies
Is it flexible in size?NoNo, fixed sizeYes, it can grow
Where do its elements live?Directly in the array objectDirectly in the array object, which may be on the stackOn the free store, reached through a handle

Three containers compared on size knowledge, copying, and pointer conversion. The array and vector rows for pointer conversion reflect the source's point that all standard containers avoid the built-in array hazard.

Key point

The source gives the main reason for preferring array over a built-in array as the way array avoids surprising and dangerous conversions to pointers, which is a reason beyond size and copying alone.

Trading Flexibility for Simplicity

Array is less flexible than vector, and because of that it is simpler. A fixed-size container does not need to manage growth, so it has fewer moving parts to reason about. There is no need to think about reallocating storage when more elements arrive, because the number of elements never changes.

Performance is the other side of the same trade. A vector acts as a handle to elements that are allocated on the free store. Reaching an element through a handle involves an extra step, and allocating and later deallocating those elements has its own cost. An array whose elements sit directly on the stack avoids both costs. The source is careful about this claim: the advantage is real in some programs and is not guaranteed in all of them.

Definition

The free store is where a vector's elements are allocated, and the vector itself holds a handle that leads to them. An array with elements on the stack needs neither the extra indirection through that handle nor the separate allocation and deallocation step.

The Cost of Stack Storage

Storing elements on the stack is not free. The stack is a limited resource, and this limitation is especially pronounced on some embedded systems. Because of that, stack overflow is a serious risk, which the source describes as nasty. A large array placed on the stack consumes stack space that the rest of the program also needs. The performance benefit of stack allocation therefore has to be weighed against the size of the data.

main framesmall localsprocess_data framelarge local arraystack limitfixed sizecallsframe grows toward
When a function declares a large local array, its frame takes up part of the fixed stack that other calls also need, and the frame can grow toward the stack limit.

The practical rule that follows is that a small, fixed number of elements is a good fit for the stack. A very large collection is a poor fit, and a container that uses the free store may be the better choice for it. The diagram shows why size matters: the large frame claims a share of a limit that is shared by every call in the program.

How Pointer Decay Loses Information

A built-in array passed to a function generally decays into a pointer to its first element. The function then sees only that pointer, and the knowledge of the array's length and element type can be lost along the way. The source describes this as a set of nasty conversions to pointers. An array can also be explicitly passed to a C-style function that expects a pointer, so the conversion is sometimes deliberate and sometimes accidental.

int array4 elementsint pointerto element 0lengthlostdecays to
When a built-in array is passed to a function, the function receives the address of element 0 and loses the element count.
cpp
Output
10
Notes on the code
  • readings is a built-in array of four ints.
  • When passed to print_first, readings decays to a pointer to readings[0].
  • print_first receives no length, so it cannot know how many elements are there.

Subscripting Through the Wrong Pointer Type

The real danger appears when a pointer of one type is used to subscript an array of a different type. Think of a pointer to a base type that is used to step through an array of a derived type. Subscripting moves the pointer by the size of the type it points to. If that stepping size does not match the size of the elements actually stored, the computed address points to the wrong place.

The source assumes that sizeof(Shape) is less than sizeof(Circle). Under that assumption, stepping a Shape pointer by one position across a Circle array does not reach the next Circle. It lands partway into the element that came before. Subscripting a Circle array through a Shape pointer therefore gives a wrong offset, and reading or writing through it touches the wrong data. The numbers used below, 8 and 16 bytes, are only illustrative. The source does not give specific sizes; what matters is the inequality and the mismatch it creates.

Circle 0bytes 0 to 15p[1] resultbyte 8, inside Circle 0Circle 1bytes 16 to 31step of 8 bytesintended step of 16 bytes
With a Shape pointer aimed at the first Circle, p[1] steps 8 bytes and lands inside Circle 0, not at the start of Circle 1 where the program intended to go.
Worked example

Tracing an Offset Through the Wrong Pointer Type

A Circle array begins at byte 0. A Shape pointer p points to its first element. Using the illustrative sizes sizeof(Shape) = 8 and sizeof(Circle) = 16, where does p[1] land, and where did the program intend to land?

  1. 1
    Find the stepping size

    Subscripting moves the pointer by the size of the type it points to, which is Shape. So each step is 8 bytes.

  2. 2
    Compute the computed offset

    p[1] is one step from p, so the computed address is 0 plus 8, which is byte 8.

  3. 3
    Locate the element at that byte

    Circle 0 occupies bytes 0 to 15. Byte 8 is inside Circle 0, halfway through it.

  4. 4
    Compare with the intended element

    The intended element is Circle 1, which begins at byte 16. Reading through p[1] therefore touches the wrong data.

Result

p[1] lands at byte 8, inside Circle 0, instead of at the start of Circle 1 at byte 16. The offset is wrong because the stepping type and the stored element type differ in size.

Mistakes Beginners Make with Arrays

  • Common mistake

    Assuming a built-in array carries its length when it is passed to a function

    What it looks like

    A function receives int* values and loops past the end because it assumes it knows the count.

    Why it fails

    A built-in array decays to a pointer to its first element, and the element count is lost in the conversion.

    Do this instead

    Pass the length as a separate argument, or use an array, which knows its size.

  • Common mistake

    Choosing stack storage for a very large collection

    What it looks like

    A function declares a huge local array and calls other functions that also need stack space.

    Why it fails

    The stack is a limited resource, and a large frame risks stack overflow.

    Do this instead

    Keep stack arrays small and fixed, and use a container that allocates on the free store for very large data.

  • Common mistake

    Subscripting an array through a pointer of a different type

    What it looks like

    A Shape pointer steps through a Circle array one Shape at a time.

    Why it fails

    Subscripting moves the pointer by the size of its own type, so a mismatch in sizes lands in the wrong place.

    Do this instead

    Subscript the array through a pointer or element type that matches what is actually stored.

  • Common mistake

    Assuming array is always faster than vector, when the speed benefit only holds for data small enough to live comfortably on the stack

Checking Your Understanding of Offsets

MEDIUM

A Circle array holds three elements. Assume sizeof(Shape) = 8 and sizeof(Circle) = 16 as illustrative values. A Shape pointer p is aimed at the first Circle. Which byte does p[2] reach, which Circle does the program intend to reach, and which Circle does p[2] actually land inside?

Hints
  • Multiply the subscript by the stepping size of the pointer's own type, not the stored element type.
  • Find which Circle's byte range contains the computed byte, and compare it with the start byte of the intended Circle.
  • The intended target is Circle 2, which begins at byte 32.

Key Takeaways

  • An array is less flexible than a vector, but its fixed size makes it simpler to reason about, and it can be faster when its elements sit directly on the stack.
  • A built-in array does not carry its length and does not copy in the same way as an array, and it decays to a pointer to its first element when passed to a function.
  • Subscripting through a pointer of a different type moves by the size of that pointer's type, so a size mismatch with the stored elements can land at the wrong element.
  • The stack is a limited resource, so a large array placed on it risks stack overflow, and very large collections are better stored in a container that uses the free store.

Related concepts

More concepts starting with B