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.
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?
Reveal 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.
| Question | Built-in array | Array | Vector |
|---|---|---|---|
| Does it know its own size? | No, length must be passed separately | Yes | Yes |
| Can it be copied with assignment or initialization? | Not in the same way | Yes | Yes |
| Does it convert to a raw pointer on its own? | Yes, it decays to a pointer to its first element | The protection against the pointer conversion hazard applies | The protection against the pointer conversion hazard applies |
| Is it flexible in size? | No | No, fixed size | Yes, it can grow |
| Where do its elements live? | Directly in the array object | Directly in the array object, which may be on the stack | On 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.
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.
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.
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.
10- readings is a built-in array of four ints.
- When passed to
print_first, readings decays to a pointer to readings[0]. print_firstreceives 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.
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?
- 1Find 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.
- 2Compute the computed offset
p[1] is one step from p, so the computed address is 0 plus 8, which is byte 8.
- 3Locate the element at that byte
Circle 0 occupies bytes 0 to 15. Byte 8 is inside Circle 0, halfway through it.
- 4Compare with the intended element
The intended element is Circle 1, which begins at byte 16. Reading through p[1] therefore touches the wrong data.
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 likeA function receives int* values and loops past the end because it assumes it knows the count.
Why it failsA built-in array decays to a pointer to its first element, and the element count is lost in the conversion.
Do this insteadPass 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 likeA function declares a huge local array and calls other functions that also need stack space.
Why it failsThe stack is a limited resource, and a large frame risks stack overflow.
Do this insteadKeep 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 likeA Shape pointer steps through a Circle array one Shape at a time.
Why it failsSubscripting moves the pointer by the size of its own type, so a mismatch in sizes lands in the wrong place.
Do this insteadSubscript 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
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.