1. Introduction to array-to-pointer conversion (decay)
There is a historical nuance with arrays in C++: the language supports a style from the times when many APIs wanted to accept "just the address of data". Therefore, in expressions an array often behaves not as a "container of N elements" but as "the address of the first element". This transformation is called array-to-pointer conversion or decay (colloquially — "the array decays to a pointer").
Why was this done? Mostly for convenience and compatibility with C: it was easier for functions and old interfaces to work with the "start of data". But convenience has a cost: an address does not carry size, whereas for an array the size is one of the main parts of its meaning.
The key idea for the whole lecture: "an array as an object" and "an array name in an expression" are not the same thing, and confusing them breaks program logic.
Array as an object: memory, elements and size
It is important to get this straight: an array is an object. It occupies memory for all elements at once and has a fixed length known at compile time.
Let's develop our project: a score table (for example, points for 5 rounds).
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
std::cout << scores[0] << ' ' << scores[N - 1] << '\n'; // 10 9
}
As long as we access scores[i] and use N, everything is transparent: there is a length, there are elements, and correct bounds 0..N-1.
While you keep the array "as an array", you can safely compute the number of elements, do sizeof(scores), use std::size(scores) and in general feel confident.
Minimal pointer model: "pointer = address"
The word "pointer" sounds like a prelude to scary stories about memory leaks, new/delete and debugger nightmares. That will not be the case here: today a pointer is just a variable that stores an address, i.e. "a location in memory" where some object resides.
A simple analogy: an array is a "shelf of books", and a pointer is a "label with the coordinate of where the first book stands". The label is useful, but if it does not state how many books are on the shelf, you might try to take the "sixth book" where the shelf already ended.
A minimal practical thought we need: if p is a pointer to int, then the expression p[0] means "the first int at that address". We do not discuss why the language is designed this way, and we do not perform pointer arithmetic — just accept that in C++ you can access data by address this way.
2. Decay in practice: array becomes address of first element
Now — the main focus of the lecture. For an array int scores[N] there is an important property:
- scores as an object is an array of N elements;
- but scores in most expressions automatically becomes int*, i.e. the address of the first element.
Practically it looks like this:
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
int* p = scores; // decay: p "points" to scores[0]
std::cout << scores[0] << ' ' << p[0] << '\n'; // 10 10
}
Here a "array → address of first element" transformation happened. Pointer p points to the same place where scores[0] resides.
If you want to see it explicitly, you can take the address of the first element directly:
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
int* p1 = scores; // implicitly (decay)
int* p2 = &scores[0]; // explicitly
std::cout << p1 << '\n'; // for example: 0x7ff... (address will be different on your machine)
std::cout << p2 << '\n'; // same address
}
std::cout for int* prints the address (usually in hexadecimal). The exact format depends on the implementation, but the idea is: it's a "memory coordinate".
5. The cost of decay: size is lost
Why you cannot "get" size from T*
When you had an array scores[N], you knew N. When all you have is int* p, you only know "where the first element is". But you do not know how many of the following elements are "yours" and how many belong to someone else.
A pointer is not a container and not a "smart structure". It is literally an address. Therefore the request "give me the size of the array that p points to" is conceptually impossible: the language does not store "array size" inside an int*.
To feel the problem hands-on, let's show a "bad idea": trying to compute the number of elements via sizeof on the pointer.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
int* p = scores;
std::size_t wrongCount = sizeof(p) / sizeof(p[0]);
std::cout << wrongCount << '\n'; // often 2 on 64-bit (8 / 4), but this is NOT the array size
}
This result is not related to the number of elements. It is related to the fact that the "address size" (for example, 8 bytes) is divided by "size of int" (for example, 4 bytes). That's accidental arithmetic, not program logic.
sizeof(array) and sizeof(pointer) are different entities
sizeof measures the size of a type/object in bytes. Here arrays and pointers differ radically:
- sizeof(scores) — size of the entire array (all elements together);
- sizeof(p) — size of the address variable (usually 4 or 8 bytes), but not of the data.
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
int* p = scores;
std::cout << "sizeof(scores) = " << sizeof(scores) << '\n';
std::cout << "sizeof(p) = " << sizeof(p) << '\n';
}
A typical picture on a 64‑bit system: sizeof(scores) will be 20 (5 * 4), while sizeof(p) will be 8. But don't attach to specific numbers: even if int has a different size, the logic remains — the array is "heavy", the pointer is "light".
How to correctly get the length of an array
Beginners often remember the formula:
sizeof(a) / sizeof(a[0])
It indeed works… as long as a is an array. As soon as a "decays" to a pointer, you divide "address size" by "element size" and get nonsense.
#include <cstddef>
#include <iostream>
int main() {
int a[5] = {1, 2, 3, 4, 5};
int* p = a;
std::size_t ok = sizeof(a) / sizeof(a[0]);
std::size_t bad = sizeof(p) / sizeof(p[0]);
std::cout << ok << ' ' << bad << '\n'; // 5 2 (bad — garbage logic)
}
A more readable way to get the number of elements is std::size(a). It understands that it was passed an array and returns the correct count.
#include <cstddef>
#include <iostream>
#include <iterator> // std::size
int main() {
int a[5] = {1, 2, 3, 4, 5};
int* p = a;
std::cout << std::size(a) << '\n'; // 5
// std::cout << std::size(p) << '\n'; // not allowed: p is not an array, it has no length
(void)p;
}
6. Practice: printing and iterating via pointer
Why std::cout << a; does not print elements
There is a funny pitfall that catches almost everyone: you do std::cout << a; and expect to see 1 2 3 4 5, but get something like 0x7ff....
The reason is the same: in the expression a it decays to int*, and operator<< for a pointer prints the address.
#include <iostream>
int main() {
int a[3] = {10, 20, 30};
std::cout << a << '\n'; // 0x7ff... (address, NOT "10 20 30")
std::cout << a[0] << '\n'; // 10
}
If you want to print the elements, print them in a loop (for now without algorithms and without range-for, so as not to get ahead of ourselves):
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 3;
int a[N] = {10, 20, 30};
for (std::size_t i = 0; i < N; ++i) {
std::cout << a[i] << ' ';
}
std::cout << '\n'; // 10 20 30
}
If you only have a T*, store the length separately
A practical rule that saves nerves: once you "step into the pointer world", the length must sit next to it.
For this lecture we keep it straightforward: there is N, there is p, and we iterate p[i], but we take the loop bound from N, not by "guessing".
#include <cstddef>
#include <iostream>
int main() {
constexpr std::size_t N = 5;
int scores[N] = {10, 12, 8, 15, 9};
int* p = scores;
int sum = 0;
for (std::size_t i = 0; i < N; ++i) {
sum += p[i];
}
std::cout << "sum=" << sum << '\n'; // sum=54
}
If you want to strengthen discipline (and reduce the chance of "accidentally changing N"), you can take the length from the array via std::size(scores), but only before you "lost arrayness":
#include <cstddef>
#include <iostream>
#include <iterator>
int main() {
int scores[5] = {10, 12, 8, 15, 9};
const std::size_t n = std::size(scores);
int* p = scores;
for (std::size_t i = 0; i < n; ++i) {
std::cout << p[i] << ' ';
}
std::cout << '\n'; // 10 12 8 15 9
}
Diagram: what exactly happens during decay
To avoid turning this into a "compiler magic ritual", it's useful to keep a simple diagram in mind: there is an array object, and there is an expression that often becomes an address.
flowchart TD
A["int scores[N] (array as an object)
has N elements"] --> B["scores (in expression)"]
B --> C["int* p = scores;
p stores address of scores[0]"]
C --> D["p[i] (access to data by address)
BUT i must be bounded separately"]
The point of this diagram is: arrays and pointers are related, but they are not the same.
7. Common mistakes
Mistake #1: thinking that T* "knows the array length".
After int* p = a; many intuitively expect that p stores not only the address but also the size. But the array size is not part of a pointer. If you iterate p in a loop, you must have the bound separately (constant N, the value of std::size(a) before decay, etc.), otherwise it's easy to go out of bounds and get undefined behavior.
Mistake #2: computing the number of elements via sizeof(p) / sizeof(p[0]).
It seems logical, until you realize that sizeof(p) is the size of the address, not of the data. The resulting number often looks "plausible" (for example, 2), which is why the mistake survives. The correct formula sizeof(a) / sizeof(a[0]) works only for a real array where a is an array object, not a pointer.
Mistake #3: writing std::cout << a; and expecting elements to be printed.
The stream output sees not an "array" but "the address of the first element" (the result of decay), so it prints the address. To print elements you need a loop and explicit output of a[i]. If you see 0x... in output, it's not that cout is broken — you asked to print an address.
Mistake #4: confusing "last index" and "size".
After switching to a pointer it's especially easy to mix up bounds: the size is N, the last index is N - 1. An error like i <= N in a loop becomes an attempt to read p[N], i.e. the "sixth element" of an array of five. Even if the program "doesn't crash", it doesn't mean it's correct: it may quietly read someone else's memory.
Mistake #5: trying to explain p[i] with pointer arithmetic and immediately start experimenting.
Yes, pointers have arithmetic, but that's a separate topic with its own traps. For this lecture stick to the minimal model: "pointer = address", " p[i] gives access to the i-th element", "bounds are given separately by size". This gives a stable base and prevents the lesson from turning into a safari through undefined behavior.
GO TO FULL VERSION