AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

06 Arrays, pointers and references / Lesson 1 of 6

Built-in arrays and std::array

14 min · +10 XP · 2 exercises · 7 questions

Read the text14 min
Exercises0/2
Quiz7 q

Quick answer

What is the difference between a C array and std::array in C++?

A C-style array (int a[5]) is a fixed-size block of elements with no bounds checking that decays to a pointer to its first element when passed to a function, losing its size. std::array<int, 5> wraps the same block with no overhead but adds value semantics: it can be copied, assigned, compared with == and returned by value, it knows its .size(), and its .at() is bounds-checked.

An array is a fixed number of elements of one type stored one after another in memory. C++ inherited the built-in array from C, and with it three properties that shape the whole language: the size is fixed at compile time, nothing checks that an index is in range, and the array's name quietly turns into a pointer the moment you pass it around. This lesson settles what a built-in array really is, where it bites, and why std::array<T, N> — the same contiguous block with value semantics and a .size() — is what modern code writes instead.

Declaring and initialising

int scores[5];                    // five ints; automatic storage, NOT initialised
int primes[5] = {2, 3, 5, 7, 11}; // aggregate initialisation
int zeros[5] = {};                // every element value-initialised to 0
int partial[5] = {1, 2};          // 1, 2, 0, 0, 0 — the rest are zeroed
int deduced[] = {4, 8, 15, 16};   // size deduced from the initialiser: 4

The size in brackets must be a constant expression: a literal, a constexpr int, an enumerator. int n; std::cin >> n; int a[n]; is a variable-length array — a C99 feature that GCC accepts as an extension and standard C++ does not have; Clang warns and other compilers reject it. When the size is only known at run time the answer is std::vector (Module 13, STL containers).

The first line above is the dangerous one: a local built-in array with no initialiser holds whatever bytes were on the stack, and reading them is undefined behaviour. int scores[5] = {}; costs nothing; write the braces every time.

Indexing, and what the language does not check

int a[4] = {10, 20, 30, 40};
std::cout << a[0] << ' ' << a[3] << '\n';   // 10 40
a[4] = 50;                                    // undefined behaviour: there is no a[4]

Indices run from 0 to N - 1. There is no check: a[4] writes four bytes past the end of a into whatever happens to be there — another local, the return address, nothing you can predict — and under -O2 the optimiser is entitled to assume it never happens and delete code around it. The array does not know its own end. You have to.

To ask an array its element count, use std::size from <iterator> (C++17). It works only while the name still denotes an array, which turns out to be a feature:

#include <iterator>
int a[4] = {10, 20, 30, 40};
for (std::size_t i = 0; i < std::size(a); ++i) std::cout << a[i] << ' ';
for (int x : a) std::cout << x << ' ';        // range-for knows the bounds too

The older sizeof a / sizeof a[0] gives the same number, and a silent wrong answer after decay; prefer std::size.

Decay: the array is not a value

A built-in array is not a first-class value. It cannot be assigned (b = a; is a compile error), cannot be returned from a function by value, and cannot be compared with ==. Worse, in almost every expression the array's name decays into a pointer to its first element:

void show(int values[], int n) {          // exactly the same as void show(int* values, int n)
    std::cout << sizeof values << '\n';   // 8 on this platform — the size of a pointer, not of the array
}

int a[4] = {1, 2, 3, 4};
show(a, 4);                               // a decays to &a[0]; the 4 has to travel separately

int values[] and int values[4] in a parameter list are both just int* — the number is ignored. That is why every C-style function that takes an array also takes a length, and why std::size(values) inside show is a compile error rather than a wrong answer: a pointer has no size to give.

Comparing two arrays with == compiles because both decay: it compares two addresses and is always false for distinct arrays (C++20 deprecates it and -Wall warns). If the size must survive a call, pass the array by reference — void show(const int (&values)[4]) — or, better, stop using built-in arrays for anything you pass around.

std::array<T, N>: the same block, done properly

#include <array>

std::array<int, 5> a{2, 3, 5, 7, 11};
std::array<int, 5> z{};                  // all zeros
std::cout << a.size() << ' ' << a[2] << ' ' << a.front() << ' ' << a.back() << '\n';   // 5 5 2 11
a.at(2) = 6;                             // bounds-checked: at(7) throws std::out_of_range
a.fill(0);                               // every element 0

std::array<int, 5> b = a;                // a real copy — value semantics
b[0] = 99;                               // a[0] is unchanged
bool same = (a == b);                    // element-wise comparison: false

std::array is a struct wrapping one built-in array: the same layout, the same zero overhead, the same fixed size — and it fixes everything above. It is copied and assigned like an int, compared element by element, passed by (const) reference without losing its size, and returned by value. .at() gives a checked access when the index came from outside; [] is still unchecked, exactly as fast as the built-in and exactly as undefined out of range.

double mean(const std::array<int, 5>& values) {   // by const reference: no copy, size known
    long long sum = 0;
    for (int x : values) sum += x;
    return static_cast<double>(sum) / values.size();
}

The one thing std::array shares with the built-in is uninitialised elements when you write std::array<int, 5> a; with no braces. Write a{}.

Built-in T a[N]std::array<T, N>std::vector<T>
Sizecompile-time, fixedcompile-time, fixedrun-time, growable
Knows its sizeonly via std::size, before decay.size().size()
Copy, assign, ==no (decays)yes, element-wiseyes, element-wise
Bounds checknone.at().at()
Storageinline (the stack, for a local)inlinethe heap, through a handle

Two-dimensional built-in arrays, in brief

int grid[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
std::cout << grid[1][2] << '\n';   // 7: row 1, column 2

A 2-D built-in array is an array of arrays, laid out row after row in one block (row-major). Passing one to a function needs every dimension but the first — void show(int g[][4], int rows) — so the compiler can compute g[r][c]. Lesson 6 covers grids properly.

Pitfalls

  • Off by one. for (int i = 0; i <= N; ++i) a[i] touches a[N]. Half-open ranges (i < N) are the habit that prevents it.
  • sizeof after decay. sizeof values / sizeof values[0] inside a function taking int values[] is 8 / 4 = 2, always. Pass the length or use std::array.
  • Uninitialised elements. int a[5]; and std::array<int, 5> a; both hold garbage. = {} and {}.
  • Returning a local array. int* f() { int a[3] = {1, 2, 3}; return a; } returns a pointer to storage that no longer exists — a dangling pointer (lesson 4 and Module 7, Memory, ownership and RAII).

Key takeaways

  • A built-in array is a fixed-size contiguous block with no bounds check; out-of-range access is undefined behaviour, not an exception.
  • Always initialise: int a[N] = {};.
  • An array's name decays to a pointer to its first element when passed; the callee loses the size, so pass it separately.
  • std::array<T, N> keeps the layout and the speed and adds value semantics, .size(), .at() and ==.
  • Fixed size at compile time: std::array. Run-time size: std::vector. A raw T a[N] only for the smallest local scratch.

Common questions

What is array decay in C++?

Array decay is the implicit conversion of an array's name to a pointer to its first element, which happens in almost every expression, including passing it to a function. The size is lost: a parameter written int values[] is really int*, so sizeof gives the pointer's size and the length must be passed separately.

What happens if you access an array out of bounds in C++?

It is undefined behaviour, not an exception. a[4] on a four-element array reads or writes whatever memory lies past the end, and the optimiser may assume it never happens. Only .at() on std::array or std::vector checks the index, throwing std::out_of_range.

How do I get the length of an array in C++?

Use std::size(a) from <iterator> (C++17) while the name still denotes an array, or .size() on a std::array. Inside a function that received the array as a parameter it has already decayed to a pointer, so std::size fails to compile and sizeof a / sizeof a[0] silently gives the wrong answer.

Are C++ arrays initialised to zero?

Not a local array without an initialiser: int a[5]; holds indeterminate values, and reading them is undefined behaviour. Write int a[5] = {}; to zero every element; int a[5] = {1, 2}; sets the first two and zeroes the rest. std::array<int, 5> a{}; zeroes too.

When should I use std::array instead of std::vector?

Use std::array when the element count is fixed at compile time: it stores its elements inline, on the stack for a local, with no heap allocation. Use std::vector when the size is only known at run time or must grow.

Mark as readSign in to keep your place.

C++ Arrays vs std::array: Decay, Size and Bounds Checking

C++ built-in arrays have a fixed size, no bounds check and decay to a pointer when passed. How std::array adds size(), at(), copying and == comparison.

  • Course: C++ study plan
  • Module: Arrays, pointers and references
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: Clang 18 · C++20

What is the difference between a C array and std::array in C++?

A C-style array (int a[5]) is a fixed-size block of elements with no bounds checking that decays to a pointer to its first element when passed to a function, losing its size. std::array<int, 5> wraps the same block with no overhead but adds value semantics: it can be copied, assigned, compared with == and returned by value, it knows its .size(), and its .at() is bounds-checked.

Lesson

An array is a fixed number of elements of one type stored one after another in memory. C++ inherited the built-in array from C, and with it three properties that shape the whole language: the size is fixed at compile time, nothing checks that an index is in range, and the array's name quietly turns into a pointer the moment you pass it around. This lesson settles what a built-in array really is, where it bites, and why std::array<T, N> — the same contiguous block with value semantics and a .size() — is what modern code writes instead.

Declaring and initialising

int scores[5];                    // five ints; automatic storage, NOT initialised
int primes[5] = {2, 3, 5, 7, 11}; // aggregate initialisation
int zeros[5] = {};                // every element value-initialised to 0
int partial[5] = {1, 2};          // 1, 2, 0, 0, 0 — the rest are zeroed
int deduced[] = {4, 8, 15, 16};   // size deduced from the initialiser: 4

The size in brackets must be a constant expression: a literal, a constexpr int, an enumerator. int n; std::cin >> n; int a[n]; is a variable-length array — a C99 feature that GCC accepts as an extension and standard C++ does not have; Clang warns and other compilers reject it. When the size is only known at run time the answer is std::vector (Module 13, STL containers).

The first line above is the dangerous one: a local built-in array with no initialiser holds whatever bytes were on the stack, and reading them is undefined behaviour. int scores[5] = {}; costs nothing; write the braces every time.

Indexing, and what the language does not check

int a[4] = {10, 20, 30, 40};
std::cout << a[0] << ' ' << a[3] << '\n';   // 10 40
a[4] = 50;                                    // undefined behaviour: there is no a[4]

Indices run from 0 to N - 1. There is no check: a[4] writes four bytes past the end of a into whatever happens to be there — another local, the return address, nothing you can predict — and under -O2 the optimiser is entitled to assume it never happens and delete code around it. The array does not know its own end. You have to.

To ask an array its element count, use std::size from <iterator> (C++17). It works only while the name still denotes an array, which turns out to be a feature:

#include <iterator>
int a[4] = {10, 20, 30, 40};
for (std::size_t i = 0; i < std::size(a); ++i) std::cout << a[i] << ' ';
for (int x : a) std::cout << x << ' ';        // range-for knows the bounds too

The older sizeof a / sizeof a[0] gives the same number, and a silent wrong answer after decay; prefer std::size.

Decay: the array is not a value

A built-in array is not a first-class value. It cannot be assigned (b = a; is a compile error), cannot be returned from a function by value, and cannot be compared with ==. Worse, in almost every expression the array's name decays into a pointer to its first element:

void show(int values[], int n) {          // exactly the same as void show(int* values, int n)
    std::cout << sizeof values << '\n';   // 8 on this platform — the size of a pointer, not of the array
}

int a[4] = {1, 2, 3, 4};
show(a, 4);                               // a decays to &a[0]; the 4 has to travel separately

int values[] and int values[4] in a parameter list are both just int* — the number is ignored. That is why every C-style function that takes an array also takes a length, and why std::size(values) inside show is a compile error rather than a wrong answer: a pointer has no size to give.

Comparing two arrays with == compiles because both decay: it compares two addresses and is always false for distinct arrays (C++20 deprecates it and -Wall warns). If the size must survive a call, pass the array by reference — void show(const int (&values)[4]) — or, better, stop using built-in arrays for anything you pass around.

std::array<T, N>: the same block, done properly

#include <array>

std::array<int, 5> a{2, 3, 5, 7, 11};
std::array<int, 5> z{};                  // all zeros
std::cout << a.size() << ' ' << a[2] << ' ' << a.front() << ' ' << a.back() << '\n';   // 5 5 2 11
a.at(2) = 6;                             // bounds-checked: at(7) throws std::out_of_range
a.fill(0);                               // every element 0

std::array<int, 5> b = a;                // a real copy — value semantics
b[0] = 99;                               // a[0] is unchanged
bool same = (a == b);                    // element-wise comparison: false

std::array is a struct wrapping one built-in array: the same layout, the same zero overhead, the same fixed size — and it fixes everything above. It is copied and assigned like an int, compared element by element, passed by (const) reference without losing its size, and returned by value. .at() gives a checked access when the index came from outside; [] is still unchecked, exactly as fast as the built-in and exactly as undefined out of range.

double mean(const std::array<int, 5>& values) {   // by const reference: no copy, size known
    long long sum = 0;
    for (int x : values) sum += x;
    return static_cast<double>(sum) / values.size();
}

The one thing std::array shares with the built-in is uninitialised elements when you write std::array<int, 5> a; with no braces. Write a{}.

Built-in T a[N]std::array<T, N>std::vector<T>
Sizecompile-time, fixedcompile-time, fixedrun-time, growable
Knows its sizeonly via std::size, before decay.size().size()
Copy, assign, ==no (decays)yes, element-wiseyes, element-wise
Bounds checknone.at().at()
Storageinline (the stack, for a local)inlinethe heap, through a handle

Two-dimensional built-in arrays, in brief

int grid[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
std::cout << grid[1][2] << '\n';   // 7: row 1, column 2

A 2-D built-in array is an array of arrays, laid out row after row in one block (row-major). Passing one to a function needs every dimension but the first — void show(int g[][4], int rows) — so the compiler can compute g[r][c]. Lesson 6 covers grids properly.

Pitfalls

  • Off by one. for (int i = 0; i <= N; ++i) a[i] touches a[N]. Half-open ranges (i < N) are the habit that prevents it.
  • sizeof after decay. sizeof values / sizeof values[0] inside a function taking int values[] is 8 / 4 = 2, always. Pass the length or use std::array.
  • Uninitialised elements. int a[5]; and std::array<int, 5> a; both hold garbage. = {} and {}.
  • Returning a local array. int* f() { int a[3] = {1, 2, 3}; return a; } returns a pointer to storage that no longer exists — a dangling pointer (lesson 4 and Module 7, Memory, ownership and RAII).

Key takeaways

  • A built-in array is a fixed-size contiguous block with no bounds check; out-of-range access is undefined behaviour, not an exception.
  • Always initialise: int a[N] = {};.
  • An array's name decays to a pointer to its first element when passed; the callee loses the size, so pass it separately.
  • std::array<T, N> keeps the layout and the speed and adds value semantics, .size(), .at() and ==.
  • Fixed size at compile time: std::array. Run-time size: std::vector. A raw T a[N] only for the smallest local scratch.

Common questions

What is array decay in C++?

Array decay is the implicit conversion of an array's name to a pointer to its first element, which happens in almost every expression, including passing it to a function. The size is lost: a parameter written int values[] is really int*, so sizeof gives the pointer's size and the length must be passed separately.

What happens if you access an array out of bounds in C++?

It is undefined behaviour, not an exception. a[4] on a four-element array reads or writes whatever memory lies past the end, and the optimiser may assume it never happens. Only .at() on std::array or std::vector checks the index, throwing std::out_of_range.

How do I get the length of an array in C++?

Use std::size(a) from <iterator> (C++17) while the name still denotes an array, or .size() on a std::array. Inside a function that received the array as a parameter it has already decayed to a pointer, so std::size fails to compile and sizeof a / sizeof a[0] silently gives the wrong answer.

Are C++ arrays initialised to zero?

Not a local array without an initialiser: int a[5]; holds indeterminate values, and reading them is undefined behaviour. Write int a[5] = {}; to zero every element; int a[5] = {1, 2}; sets the first two and zeroes the rest. std::array<int, 5> a{}; zeroes too.

When should I use std::array instead of std::vector?

Use std::array when the element count is fixed at compile time: it stores its elements inline, on the stack for a local, with no heap allocation. Use std::vector when the size is only known at run time or must grow.

Exercises

Reverse in place

Read n (1 ≤ n ≤ 50) and then n integers into a std::array<int, 50>, keeping n as the count of elements in use. Reverse the first n elements in place — two indices walking towards each other, swapping with std::swap — and print them space-separated on one line. Do not build a second array.

Input: n, then n integers (whitespace-separated, possibly across several lines). Output: the n integers in reverse order on one line.

Example: input 5 1 2 3 4 5 → output 5 4 3 2 1

Above the average

Read n (1 ≤ n ≤ 100) integers into a built-in array int values[100]. Write double average(const int* values, int n) — the array decays to a pointer when you pass it, so the count travels alongside — and use it to print the mean to two decimals and how many values are strictly greater than the mean.

Input: n, then n integers. Output: two lines, average=<mean with 2 decimals> and above=<count>.

Example: input 4 10 20 30 41 →

average=25.25
above=2

In this module: Arrays, pointers and references

← Checkpoint — Strings · Pointers — addresses, nullptr and the arrow →