Skip to content
VLSI Mentor

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:

ChapterBorrowed forWhat it used
5.6 §2something to iterateint delays[] = {3, 7, 2, 9};
6.2 §5something expensive to copyan array inside a struct
8.4 §2something contiguous to walkint 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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 deduced

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
arrays.cpp — complete program
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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? true

Every 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

Four boxes in a row, left to right, representing the sixteen bytes of an array of four ints. The first box is delays index zero, holding the value three, at byte offset zero. The second is delays index one, holding seven, at offset four. The third is delays index two, holding two, at offset eight. The fourth is delays index three, holding nine, at offset twelve. The boxes are adjacent with no gaps between them, because the elements are stored one immediately after another and the array carries no length field of its own.3[0] ·byte 07[1] ·byte 42[2] ·byte 89[3] ·byte 1212
Figure 1 — the sixteen bytes of int delays[4] = {3, 7, 2, 9}, with the measured offsets from the array's own address. There is no length field and no header: the array is exactly four ints end to end, which is why its address and its first element's address are the same number and why the count has to live in the type rather than in memory.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    delays[2]        // the third element

Chapter 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int delays[4] = {3, 7, 2, 9};
    std::cout << delays[9] << '\n';
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
runoob.cpp — complete program
#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;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic runoob.cpp -o runoob
$                                            ← zero warnings

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
==56165==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00016b0627e4
SUMMARY: AddressSanitizer: stack-buffer-overflow runoob.cpp:7 in main

Caught, with the line.

Case C — through a pointer. Not caught either. This is Chapter 8.4 §6's exact measurement:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int* p = delays + 9;
    *p = 1;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic B.cpp -o B
$                                            ← zero warnings

7. Why the Split Is Exactly What Chapter 8.4 Predicted

Put the three cases together:

FormCompilerReason
delays[9] — literal indexwarns -Warray-boundsthe type is int[4]; the length is there
delays[i] — runtime indexsilentthe length is there, the index is not
*(delays + 9) — via a pointersilentthe 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

  1. An array is a fixed number of objects, all of one type, stored one immediately after another.
  2. The count is part of the type — delays has type int[4].
  3. The count must be a compile-time constant; a runtime size needs a container.
  4. Measured: sizeof(delays) 16, one element 4, so 4 elements; sizeof a / sizeof a[0] is the idiom.
  5. Measured: addresses …860, …864, …868, …86c — exactly 4 bytes apart, no gaps.
  6. Measured: the array's address equals its first element's address — there is no header or length field.
  7. Indexing is arithmetic: a[i] is *(a + i) — Chapter 8.4 §3.
  8. So an out-of-range index is arithmetic that lands elsewhere, not a failed lookup.
  9. Measured — literal index: warning: array index 9 is past the end of the array (that has type 'int[4]').
  10. Measured — runtime index: zero warnings, and ASan reports stack-buffer-overflow with the line.
  11. Measured — via a pointer: zero warnings, which is Chapter 8.4 §6's case.
  12. 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.
  13. 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.
  14. 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.