C++ Part I · Module 9 · Arrays, Strings, and Aggregate Data
Built-in Arrays: Contiguous Storage of One Type
Four chapters have now borrowed from this module, three of them taking a built-in array because they needed something to iterate, walk, or point into. Each borrowing was declared and each pointed here. This is where the array stops being a loan and gets its definition, which is shorter than its reputation suggests: a fixed number of objects, all of the same type, stored one immediately after another. Every part of that sentence is measurable and this chapter measures all of it — the count is in the type, the elements are exactly one element-size apart, and the array's own address is its first element's address. What the chapter spends its second half on is the thing the frozen curriculum asks for by name, which is that C++ does not check whether your index is in range. That turns out to be more interesting than a flat statement, because there is one case where the compiler does check and tells you, one where it cannot and says nothing, and a tool that catches the second. The reason for the split is exactly the mechanism Chapter 8.4 named when it could not demonstrate it: an array's type carries its length, and a pointer's type does not.
Foundation16 min readC++Part IFundamentalsArrays
Part I · Module 9 · Chapter 9.1 · Built-in Arrays: Contiguous Storage of One Type
1. The Debt This Chapter Pays
Three earlier chapters borrowed an array and said so:
| Chapter | Borrowed for | What it used |
|---|---|---|
| 5.6 §2 | something to iterate | int delays[] = {3, 7, 2, 9}; |
| 6.2 §5 | something expensive to copy | an array inside a struct |
| 8.4 §2 | something contiguous to walk | int ai[4] and p + 1 |
Each used the declaration and nothing else. Here is the definition they were drawing on:
An array is a fixed number of objects, all of the same type, stored one immediately after another.
Three clauses, and §3 measures all three.
2. Declaring One
int delays[4] = {3, 7, 2, 9}; // four ints
int counts[8]{}; // eight ints, all zero
int sizes[] = {8, 16, 32}; // three ints -- the count is deducedThe count is part of the type. delays has type int[4], not "array of int". That matters more than it looks and it is the fact §7 turns on.
The count must be known at compile time. An array's size is fixed when the program is compiled; you cannot write int a[n] with a runtime n. When the count is only known at run time you need a container, which is Module 11's subject.
int counts[8]{} zeroes every element. Without the braces the elements of a local array are uninitialized, and Chapter 3.8's rule applies to each one — reading them is undefined behaviour. The braces cost two characters.
3. Measured: Fixed, Same Type, Back to Back
#include <iostream>
int main()
{
int delays[4] = {3, 7, 2, 9};
std::cout << "sizeof(delays) = " << sizeof(delays) << " bytes\n";
std::cout << "sizeof(delays[0]) = " << sizeof(delays[0]) << " bytes\n";
std::cout << "elements = " << sizeof delays / sizeof delays[0] << "\n\n";
for (int i = 0; i < 4; ++i)
std::cout << " delays[" << i << "] = " << delays[i]
<< " at " << static_cast<const void*>(&delays[i]) << '\n';
std::cout << "\nfirst element address == array address? " << std::boolalpha
<< (static_cast<const void*>(&delays[0]) == static_cast<const void*>(&delays)) << '\n';
return 0;
}Measured output, zero warnings:
sizeof(delays) = 16 bytes
sizeof(delays[0]) = 4 bytes
elements = 4
delays[0] = 3 at 0x16b31e860
delays[1] = 7 at 0x16b31e864
delays[2] = 2 at 0x16b31e868
delays[3] = 9 at 0x16b31e86c
first element address == array address? trueEvery clause of the definition is in that output.
"A fixed number": sizeof(delays) is 16 and one element is 4, so there are 4. The expression sizeof delays / sizeof delays[0] is the idiom for recovering the count, and it works because both sizes are known at compile time.
"All of the same type": one element size serves for all of them, which is what makes the division meaningful.
"One immediately after another": …860, …864, …868, …86c — each exactly 4 bytes after the last, with no gaps. This is why Chapter 8.4's p + 1 scaled by sizeof(T): the next element really is the next sizeof(T) bytes.
And the array's address is its first element's address — true. There is no header, no length field, no bookkeeping. An array of four ints is sixteen bytes of ints and nothing else, which is the property §7 depends on and the reason Chapter 9.2's problem exists.
4. Picture the Exact Bytes
The figure has no arrows because nothing flows — it is a picture of storage at one moment. What it is worth looking at is the absence: there is nowhere in those sixteen bytes for the number 4 to be recorded.
5. Indexing, and What It Really Is
delays[2] // the third elementChapter 8.4 §3 established the definition: p[i] is *(p + i). That applies here, because an array used in an expression yields a pointer to its first element — which is Chapter 9.2's subject. So delays[2] is *(delays + 2), which is the object 2 × 4 = 8 bytes past the start.
Two consequences follow immediately, and they are the rest of this chapter.
Indexing is arithmetic, not a lookup. There is no table consulted and no check performed. The index is multiplied by the element size and added.
So an out-of-range index is just arithmetic that lands somewhere else. delays[9] computes an address 36 bytes past the start and reads 4 bytes there. Whatever is at that address gets read.
6. C++ Does Not Check Your Index
The frozen purpose states this flatly, and the honest version has three cases.
Case A — a literal index the compiler can see. This one is caught.
int delays[4] = {3, 7, 2, 9};
std::cout << delays[9] << '\n';warning: array index 9 is past the end of the array (that has type 'int[4]') [-Warray-bounds]Note what the message quotes back: int[4]. The length is in the type, so the compiler can compare 9 against 4 and object.
Case B — a runtime index. This one is not.
#include <iostream>
int main()
{
int delays[4] = {3, 7, 2, 9};
int i = 0;
std::cin >> i; // the compiler cannot see this
std::cout << delays[i] << '\n';
return 0;
}$ c++ -std=c++17 -Wall -Wextra -Wpedantic runoob.cpp -o runoob
$ ← zero warningsThe length is still in the type; the index is not available, so there is nothing to compare. This is the same compile-time/run-time split Chapter 4.10 §7 drew and Chapter 5.5 §6 ran into — the compiler warns when it can see the value.
Under AddressSanitizer, with 9 on standard input:
==56165==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00016b0627e4
SUMMARY: AddressSanitizer: stack-buffer-overflow runoob.cpp:7 in mainCaught, with the line.
Case C — through a pointer. Not caught either. This is Chapter 8.4 §6's exact measurement:
int* p = delays + 9;
*p = 1;$ c++ -std=c++17 -Wall -Wextra -Wpedantic B.cpp -o B
$ ← zero warnings7. Why the Split Is Exactly What Chapter 8.4 Predicted
Put the three cases together:
| Form | Compiler | Reason |
|---|---|---|
delays[9] — literal index | warns -Warray-bounds | the type is int[4]; the length is there |
delays[i] — runtime index | silent | the length is there, the index is not |
*(delays + 9) — via a pointer | silent | the pointer's type is int*; the length is gone |
Chapter 8.4 §6 said it without being able to show it: "a pointer's bounds are not part of its type… a pointer carries an address and nothing about how far it may legally move." That is the third row. The first row is the same statement from the other side — an array's bounds are part of its type, which is the only reason any of this can be checked at all.
The length lives in the type, not in memory. So a check is possible exactly when the type is still an array type and the index is a constant, and impossible otherwise.
And Chapter 9.2 is about the moment the type stops being an array type, which is the single most common way the length gets lost.
8. What to Actually Do
Always initialize. int counts[8]{} zeroes; without braces the elements are indeterminate — Chapter 3.8.
Recover the count with sizeof a / sizeof a[0] — and only while you still have the array, which Chapter 9.2 is about.
Prefer a range-based for. Chapter 5.6 removes the index and therefore the out-of-range question entirely.
Prefer std::array or std::vector in new code — Module 11. The length travels with the data.
Do not rely on the compiler for bounds. Measured: it catches a literal index and nothing else.
Run -fsanitize=address in testing. It caught the runtime case with a line number.
Remember the count is part of the type, which is both why a check is ever possible and why Chapter 9.2's problem exists.
9. Mistakes Worth Making Once
Leaving a local array uninitialized. Every element is indeterminate — Chapter 3.8.
Expecting an out-of-range index to be caught. Measured: only the literal form warns.
Expecting an array to know its own length at run time. It does not — the count is in the type, and Figure 1's sixteen bytes have nowhere to put it.
Writing int a[n] with a runtime n. The size must be a compile-time constant; a container is the answer.
Assuming delays[9] reads "nothing". It reads four bytes 36 bytes past the start, and the value it printed differed between builds.
Reading a value obtained out of range and believing it. The program printed 1 in one build and 0 in another.
Thinking the pointer form is safer. It is strictly worse — the length is gone from the type.
10. What You Should Be Able to Say Now
- An array is a fixed number of objects, all of one type, stored one immediately after another.
- The count is part of the type —
delayshas typeint[4]. - The count must be a compile-time constant; a runtime size needs a container.
- Measured:
sizeof(delays)16, one element 4, so 4 elements;sizeof a / sizeof a[0]is the idiom. - Measured: addresses
…860,…864,…868,…86c— exactly 4 bytes apart, no gaps. - Measured: the array's address equals its first element's address — there is no header or length field.
- Indexing is arithmetic:
a[i]is*(a + i)— Chapter 8.4 §3. - So an out-of-range index is arithmetic that lands elsewhere, not a failed lookup.
- Measured — literal index:
warning: array index 9 is past the end of the array (that has type 'int[4]'). - Measured — runtime index: zero warnings, and ASan reports
stack-buffer-overflowwith the line. - Measured — via a pointer: zero warnings, which is Chapter 8.4 §6's case.
- The length lives in the type, not in memory — so a check is possible only while the type is an array type and the index is constant.
- Unchecked indexing was a deliberate performance trade; the modern answer is types that carry their length —
std::array,std::vector,std::string,std::string_view. - Prefer a range-based
for, and run a sanitizer, because two of the three forms have no compile-time defence.
11. Exercises
Exercise 1 — state the definition. Define a built-in array in one sentence, then name which part of your sentence each of §3's three measurements verifies.
Exercise 2 — give the sizes. For double d[6], give sizeof(d), sizeof(d[0]), the element count, and the byte offset of d[4].
Exercise 3 — predict the diagnostic. For int a[4], say what -Wall -Wextra -Wpedantic reports and what ASan reports: (a) a[2] (b) a[4] (c) a[i] with i read from input (d) int* p = a + 4; *p = 1;.
Exercise 4 — read the figure. From Figure 1, say what is at byte offset 8, how many bytes the whole array occupies, and where the number 4 is recorded.
Exercise 5 — initialize them. Give the contents after each declaration: (a) int a[3]; as a local (b) int b[3]{}; (c) int c[3] = {1}; (d) int d[] = {1, 2};.
Exercise 6 — why the split. Explain in two sentences why the compiler can check a[9] and cannot check *(a + 9), and name the chapter that stated this mechanism before it could be shown.
Exercise 7 — choose the replacement. For each, give the modern type and say what it adds: (a) four fixed delays (b) a list of samples whose length is read from a file (c) a name you own (d) a read-only view of part of a name.
12. Summary
- A fixed number of objects, all one type, stored back to back — and the count is in the type.
- Measured: 16 bytes, 4 per element, addresses 4 bytes apart, array address == first element's address.
- No header and no length field — Figure 1's sixteen bytes have nowhere to record the count.
- Indexing is arithmetic (
a[i]is*(a + i)), so out of range just lands elsewhere. - Measured: a literal index warns
-Warray-bounds; a runtime index is silent and ASan catches it; via a pointer is silent. - The length is in the type, not in memory — which is exactly Chapter 8.4 §6's mechanism from the other side.
- Unchecked indexing was a deliberate trade; Chapter 25.3 is the bug class.
- The modern answer is types that carry their length —
std::array,std::vector,std::string,std::string_view. - Prefer a range-based
for, always initialize, and run a sanitizer.
§3's last measurement said an array's address is its first element's address, and §7 said the length lives in the type. Put those two facts next to a function call and a problem appears that this chapter carefully avoided: it never passed an array to a function. The next chapter does, and the result is that the parameter is not an array at all. Its type is a pointer, which means the length is gone — and sizeof inside the function returns 8 rather than 16, for an array the caller knows is sixteen bytes long. The compiler warns about that one specific symptom and about nothing else that follows from it.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
