At this point, we’ve covered how to create both dynamic arrays using std::vector
, and statically-sized arrays using std::array
.
However, there is another, older way to create arrays in C++. These are commonly called C-style arrays.
Where possible, we should avoid using them. They have many problems, and this lesson will cover some of them. However, they are a fundamental, built-in part of the language, so we should familiarise ourselves with them.
They crop up all the time when integrating with other APIs and libraries, so we’ll regularly encounter them.
C-style arrays are built right into the language - no #include
directives are needed.
To create a C-style array called MyArray
that can contain 5 integers, we would do this:
int MyArray[5];
We can provide initial values at the same time:
int MyArray[5]{1, 2, 3, 4, 5};
We can also let the compiler deduce the length of the array when we’re providing initial values:
// Will have a length of 5
int MyArray[]{1, 2, 3, 4, 5};
Similar to std::vector
and std::array
, access to array elements is available through the []
 syntax:
int main(){
int MyArray[]{1, 2, 3, 4, 5};
int FirstElement{MyArray[0]};
MyArray[1] = 100;
MyArray[4] = 200;
}
Ensuring the index is within range is the responsibility of the developer. There is no equivalent to the at()
 method.
We can iterate over all elements of a C-style array in any of the usual ways. Normally, we’ll use a range-based for loop:
#include <iostream>
int main(){
int MyArray[]{1, 2, 3, 4, 5};
for (auto i : MyArray) { std::cout << i; }
}
12345
We cover range-based for loops in more detail a little later in this chapter.
We can find out how many elements our array contains by passing it to the std::size()
 function:
#include <iostream>
int main(){
int SomeArray[]{1, 2, 3, 4, 5};
std::cout << "Length: "
<< std::size(SomeArray);
}
Length: 5
The sizeof()
operator will return how many bytes our array is using in memory:
#include <iostream>
int main(){
int SomeArray[]{1, 2, 3, 4, 5};
std::cout << "Size: " << sizeof(SomeArray);
}
Size: 20
In this case, the array is consuming 20
bytes. This is because it has 5 integers and, on the environment this code was run on, an int
is 4Â bytes.
The sizeof
operator gives us an alternative way of determining how many objects are in our collection.
We do this by determining the memory size of the array, and dividing it by the memory size of the object type it is storing. This technique is rarely recommended over using std::size()
, but remains in common use:
#include <iostream>
int main(){
int SomeArray[]{1, 2, 3, 4, 5};
std::cout
<< "Length: "
<< sizeof(SomeArray) / sizeof(int);
}
Length: 5
Ideally, if we want to refer to the length of the array elsewhere in our code, we would simply extract it out as a constexpr
variable, which we can use to initialize the array, and then reuse as needed:
#include <iostream>
int main(){
constexpr std::size_t Length{5};
int SomeArray[Length]{1, 2, 3, 4, 5};
std::cout << "Length: " << Length;
}
Length: 5
A frustrating quirk with C-style arrays is their tendency to lose track of their size. This happens when passing an array to a function, for example:
#include <iostream>
void SomeFunc(int Array[]){
std::cout << "\nThe size is now "
<< sizeof(Array) << "?!";
}
int main(){
int Array[]{1, 2, 3, 4, 5};
std::cout << "The size is " << sizeof(Array);
SomeFunc(Array);
}
The size is 20
The size is now 8?!
This behavior is referred to as decaying to a pointer. The 8
in this output is the size of a pointer (8 bytes) in the environment the code was run.
Specifically, the array has decayed to a pointer to the first element of the array. We can safely access the first element by dereferencing the pointer, as a C-style array can’t have a length of 0
.
However, without knowing how many more elements are in the array, we can’t do much else. For example, attempting to iterate over the array using a range-based for loop will throw a compilation error.
To counter this, we need to keep track of the array’s length separately, passing it around to anywhere it is needed. That could mean, for example, adding additional parameters to functions that receive C-style arrays.
void SomeFunc(int Array[], std::size_t Length);
We still can’t directly use a range-based for loop, but at least we now have the information we need to create a standard for
 loop:
#include <iostream>
void SomeFunc(int Array[], std::size_t Length){
for (std::size_t i{0}; i < Length; ++i) {
std::cout << Array[i] << ", ";
}
}
int main(){
int Array[]{1, 2, 3, 4, 5};
SomeFunc(Array, 5);
}
1, 2, 3, 4, 5,
C-style arrays are statically sized. The size must be known at compile time
If we need to "resize" a C-style array, we do that manually by allocating a new array and then copying the existing elements over to the new memory location.
The std::memcpy()
function can help us with this. It copies bytes from one memory location to another. It accepts three arguments:
If our array hasn’t decayed to a pointer, we can get the number of bytes using the sizeof()
 function:
#include <iostream>
int main(){
int SmallArray[]{1, 2, 3, 4, 5};
int BigArray[6];
std::memcpy(BigArray, SmallArray,
sizeof(SmallArray));
BigArray[5] = 6;
for (auto i : BigArray) {
std::cout << i << ", ";
}
}
1, 2, 3, 4, 5, 6,
If it has decayed to a pointer, we can calculate the number of bytes in memory by multiplying the array length by the sizeof()
the data type it contains:
#include <iostream>
void SomeFunction(int SmallArray[],
std::size_t Length){
int BigArray[6];
std::memcpy(BigArray, SmallArray,
Length * sizeof(int));
BigArray[5] = 6;
for (auto i : BigArray) {
std::cout << i << ", ";
}
}
int main(){
int SomeArray[]{1, 2, 3, 4, 5};
SomeFunction(SomeArray, 5);
}
1, 2, 3, 4, 5, 6,
Above, we’ve already highlighted some problems with C-style arrays, compared to standard library containers such as std::array
and std::vector
:
[]
operatorstd::vector
comes with that capability built-inThere are yet more advantages of std::array
and std::vector
. This includes their native support for concepts such as iterators and ranges, and their compatibility with standardized algorithms which are all things we will cover later in this course.
Because of these factors, we should almost always avoid C-style arrays. Often, it’s unavoidable - we’ll be working with third-party libraries or platforms that provide C-style arrays, or expect us to provide C-style arrays as function arguments.
But, in general, we should avoid creating C-style arrays as much as possible.
When we have a C-style array, we may want to convert it to one of the friendlier library containers, such as a std::vector
or a std::array
.
Most sequential containers have a constructor that accepts two iterators and will copy everything from the first iterator to the second iterator into their container.
We cover iterators in more detail later in this chapter, but for now, we can note that iterators can be created from pointers.
Because of this, we can create sequential containers by passing two pointers to their constructor. Specifically, we pass:
The constructor will then iterate through every object in that range, taking a copy:
#include <iostream>
#include <vector>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::vector<int> Vec{Values, Values + 5};
for(int x : Vec){
std::cout << x << ", ";
}
}
1, 2, 3, 4, 5,
However, copying every object in an array can be an expensive operation, so we should only do it sparingly.
The next lesson introduces spans, which allow us to bypass many of the problems of a C-style array, without the performance cost of converting it to a totally different type of container.
In this lesson, we've explored C-style arrays in C++, covering their creation, usage, and the intricacies of managing them effectively.
We've also discussed why modern C++ alternatives such as std::vector
are generally preferred, while acknowledging scenarios where C-style arrays are still sometimes required.
#include
directives.[]
syntax.sizeof()
function.std::memcpy()
.std::array
and std::vector
, including a lack of bounds checking and manual resizing.std::vector
using iterators.A detailed guide to working with classic C-style arrays within C++, and why we should avoid them where possible
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.