C-Style Arrays

A detailed guide to working with classic C-style arrays within C++, and why we should avoid them where possible
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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.

Creating C-Style Arrays

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};

Accessing Members

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.

Iteration

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.

Getting the Size / Length

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

C-Style Arrays Decay to Pointers

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,

Resizing

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:

  • A pointer to the destination where we want things to be copied to
  • A pointer to the source we want things to be copied from
  • The number of bytes to copy

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,

More Reasons to Avoid C-Style Arrays

Above, we’ve already highlighted some problems with C-style arrays, compared to standard library containers such as std::array and std::vector:

  • They can not perform bounds checking on the indexes we pass to the [] operator
  • Resizing an array is something we have to do manually, whilst a std::vector comes with that capability built-in
  • They forget their size, requiring us to introduce additional code to keep track

There 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.

Converting to Other Containers

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 pointer to the where in memory the array begins - that is, the first element
  • the pointer to where in memory the array ends - that is, the location after the last element

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.

Summary

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.

Key Learning Points:

  • Creation of C-style arrays as a C++ language feature - without the need for #include directives.
  • Accessing and modifying elements using the [] syntax.
  • Iterating over C-style arrays with standard loops and understanding their limitations in range-based loops.
  • Calculating the size or length of C-style arrays using the sizeof() function.
  • The concept of C-style arrays decaying to pointers, especially when passed to functions.
  • Techniques for "resizing" C-style arrays using manual memory allocation and std::memcpy().
  • The drawbacks of C-style arrays compared to std::array and std::vector, including a lack of bounds checking and manual resizing.
  • Converting C-style arrays into standard library containers like std::vector using iterators.

Was this lesson useful?

Next Lesson

Array Spans and std::span

A detailed guide to creating a "view" of an array using std::span, and why we would want to
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

C-Style Arrays

A detailed guide to working with classic C-style arrays within C++, and why we should avoid them where possible

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Arrays and Linked Lists
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Array Spans and std::span

A detailed guide to creating a "view" of an array using std::span, and why we would want to
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved