Raw arrays in modern C++ are the fixed-size, built-in sequence type: a contiguous run of elements of one type, with no bookkeeping attached. They are still the right answer for a small fixed set of values where the size is a compile-time constant, but almost everything else in modern C++ reaches for [[std::array]] or [[std::vector]] instead.
Declaring a raw array
A raw array declares a type with a fixed number of elements. The size must be a constant expression, known at compile time.
#include
int main() {
int shelf[5] = {0}; // five ints, all zeroed
int ratings[4] = {4, 5, 3, 5}; // partially initialised
int fixed[3] = {1, 2, 3}; // fully initialised
char initials[3] = {'A', 'B', 'C'};
std::cout << "shelf size: " << sizeof(shelf) / sizeof(shelf[0]) << '\n';
std::cout << "ratings[1]: " << ratings[1] << '\n';
std::cout << "fixed[2]: " << fixed[2] << '\n';
std::cout << "initial: " << initials[0] << '\n';
return 0;
}Three things to notice. The elements of a raw array of a class type with a default constructor are default-initialised, but a raw array of built-in types such as int or char is left uninitialised, which means it holds whatever was in memory. Braced initialisation fills any elements you leave out with zero, so ratings is 4, 5, 3, 0.
The size is part of the type
An array's size is baked into its type. int a[5] and int a[10] are different types, and the compiler knows exactly how many elements each holds.
#include
int main() {
int small[3] = {1, 2, 3};
int large[3] = {4, 5, 6};
std::cout << "small size: " << sizeof(small) / sizeof(small[0]) << '\n';
std::cout << "large size: " << sizeof(large) / sizeof(large[0]) << '\n';
std::cout << "element size: " << sizeof(small[0]) << " bytes\n";
return 0;
}The output confirms that both arrays hold three elements and that an int takes four bytes on this machine:
small size: 3
large size: 3
element size: 4 bytesDividing sizeof of the array by sizeof of one element is the classic way to get the element count, because sizeof on an array gives the size of the whole thing. In C++17 and later, std::size from <iterator> does the same job more clearly.
Arrays are contiguous and cache friendly
The elements of a raw array sit next to each other in memory. That has one big practical consequence: if you know the index of one element, the compiler can work out the address of any other with a multiplication, and the CPU's prefetcher loves the linear access pattern.
That is why a raw array is still the right choice for a lookup table of known size, a small fixed buffer of numbers, or a structure-of-arrays layout in performance-sensitive code. It is also why std::array is usually the better answer: it has the same layout and the same performance, with a size the compiler can see and a much friendlier interface.
Array initialisation has rules worth knowing
Braced initialisation behaves in a specific way, and the differences catch people out. A partially initialised array is zero-filled for the elements you leave out, a completely empty braced list zero-fills everything, and a string literal fills a character array and pads the rest with zeros.
#include
int main() {
int zeros[4] = {}; // every element is 0
int partial[4] = {1, 2}; // 1, 2, 0, 0
char text[6] = "Hi"; // H, i, then three zero bytes
for (int i = 0; i < 4; ++i) {
std::cout << partial[i] << ' ';
}
std::cout << '\n';
std::cout << "zeros: ";
for (int i = 0; i < 4; ++i) {
std::cout << zeros[i] << ' ';
}
std::cout << '\n';
for (int i = 0; i < 6; ++i) {
std::cout << static_cast(text[i]) << ' ';
}
std::cout << '\n';
return 0;
}The output shows the zero padding and the terminated string:
1 2 0 0
zeros: 0 0 0 0
72 105 0 0 0 0The numbers in the second line are the character codes, because a char printed on its own is treated as a letter rather than a number.
The two serious drawbacks
Raw arrays come with two problems that have nothing to do with performance.
The first is that the compiler cannot check your index. Writing ratings[10] on a three-element array is , and the program may appear to work for a long time before it corrupts something.undefined behaviour
#include
int main() {
int ratings[3] = {4, 5, 3};
// Deliberately out of range. Undefined behaviour: the program
// is allowed to do anything at all.
std::cout << "out of range: " << ratings[7] << '\n';
return 0;
}The second is that an array does not know its own size. When you pass one to a function it decays into a pointer, and the length has to travel separately.
Raw arrays in modern code
There are still good reasons to reach for them.
- A compile-time constant table of values that never changes size.
- A raw buffer that you fill yourself, with the size managed by the surrounding code.
- Interfacing with C libraries whose APIs take a pointer and a length.
- Performance-critical inner loops where you want to guarantee no abstraction overhead.
In every other case, std::array gives you the same memory layout with bounds checking at compile time and a size() method.
#include
#include
int main() {
std::array ratings = {4, 5, 3, 5};
std::cout << "count: " << ratings.size() << '\n';
std::cout << "first: " << ratings.front() << '\n';
std::cout << "last: " << ratings.back() << '\n';
for (int r : ratings) {
std::cout << "rating " << r << '\n';
}
return 0;
}Quick reference
- A raw array has a fixed size that is part of its type and must be known at compile time.
- Built-in element types are left uninitialised unless you use braces.
- [[sizeof(array) / sizeof(array[0])]] gives the element count;
std::sizeis clearer. - Indexing out of range is
and the compiler will not stop you.undefinedbehaviour - An array does not know its own size once it is passed to a function.
- Raw arrays are worth it for fixed tables and raw buffers;
std::arrayis better for everything else.
Try it yourself: write a program that stores the shelf positions of five books in a raw array and prints the titles in order. Then resize it to fifty books by changing one number, and watch every place that assumed five need updating. Finally, convert the same array to [[std::array]] and use [[.size()]] instead of the [[sizeof]] division.