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 it’s important we understand them. They crop up all the time when integrating with other APIs and libraries, and we’ll see them constantly being used when we’re researching new topics.
C-style arrays are built right into the language - no #include
directives are needed.
To create a C-style array 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.
The standard way we’d 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 course.
The sizeof()
function will return how many bytes an expression or type 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.
If we lose track of how many items are in our array, we can use some sizeof()
arithmetic to find out:
#include <iostream>
int main(){
int SomeArray[]{1, 2, 3, 4, 5};
std::cout
<< "Length: "
<< sizeof(SomeArray) / sizeof(int);
}
Length: 5
More commonly, if we want to refer to the length of the array, we would simply extract it out as a constexpr
variable, which we can use to initialize the array, and then reuse elsewhere 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
. But 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 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);
}
12345
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; }
}
123456
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);
}
123456
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.
std::vector
and std::array
containers from C-style arraysWhen we have a C-style array, we may want to convert it to one of the friendlier standard library containers, such as a vector or an array.
Both of those containers have a constructor that accepts two iterators and will copy everything from the first pointer to the second pointer into their container.
Iterators can be created from pointers, so the following example demonstrates how we can create a std::vector
using the contents of a C-style array, by passing two pointers:
#include <iostream>
#include <vector>
int main(){
int Values[]{1, 2, 3, 4, 5};
std::vector<int> Vec{Values, Values + 5};
std::cout << "Vector Size: " << Vec.size();
}
Vector Size: 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.
— Added section on converting C-style arrays to standard library containers
— First Published
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.