Introduction to C-style arrays

Now that we've explored std::vector and std::array, let's complete our array coverage by examining C-style arrays. C-style arrays come directly from the C language and are built into C++ itself, unlike the standard library container classes. This means no header file inclusion is required to use them.

C-style arrays as the foundation

Because C-style arrays are the only array type directly supported by the language, the standard library containers like std::array and std::vector are typically implemented using C-style arrays under the hood.

Declaring a C-style array

C-style arrays use special syntax with square brackets ([]) to indicate an array declaration. Inside the brackets, you can optionally specify the array length, which must be an integral value of type std::size_t telling the compiler how many elements the array contains.

Here's an example creating a C-style array variable named temperatures with 7 elements of type double:

int main()
{
    double temperatures[7] {};      // Defines a C-style array named temperatures that contains 7 value-initialized double elements

//  std::array<double, 7> temps{}; // For comparison, here's a std::array of 7 value-initialized double elements

    return 0;
}

The array length must be at least 1. The compiler will produce an error if you specify zero, a negative number, or a non-integral value.

The array length must be a constant expression

Just like std::array, when declaring a C-style array, the length must be a constant expression of type std::size_t.

Warning
Some compilers may allow arrays with non-constexpr lengths for compatibility with a C99 feature called variable-length arrays (VLAs). These are not valid C++ and should not be used. If your compiler accepts these, you likely need to disable compiler extensions.

Subscripting a C-style array

Like std::array, C-style arrays support the subscript operator (operator[]) for indexing:

#include <iostream>

int main()
{
    int data[5]; // define an array of 5 int values

    data[2] = 42; // use subscript operator to index array element 2
    std::cout << data[2]; // prints 42

    return 0;
}

Unlike standard library containers (which use unsigned std::size_t indices only), C-style array indices can be any integral type (signed or unsigned) or an unscoped enumeration. This means C-style arrays avoid the sign conversion issues that affect standard library containers.

#include <iostream>

int main()
{
    const int data[] { 10, 20, 30, 40, 50 };

    int index { 3 };
    std::cout << data[index] << '\n'; // okay to use signed index

    unsigned int uIndex { 3 };
    std::cout << data[uIndex] << '\n'; // okay to use unsigned index

    return 0;
}
Warning
`operator[]` performs no bounds checking. Passing an out-of-bounds index results in undefined behavior.

Aggregate initialization of C-style arrays

C-style arrays are aggregates, so they can be initialized using aggregate initialization with an initializer list (a brace-enclosed, comma-separated list of values):

int main()
{
    int powers[6] = { 1, 2, 4, 8, 16, 32 }; // copy-list initialization using braced list
    int cubes[5] { 1, 8, 27, 64, 125 };     // list initialization using braced list (preferred)

    return 0;
}

Each initialization form initializes array members in sequence, starting with element 0.

Without an initializer, C-style array elements are default initialized, which typically leaves them uninitialized. To ensure initialization, value-initialize using empty braces:

int main()
{
    int data1[5];    // Members default initialized (int elements are left uninitialized)
    int data2[5] {}; // Members value initialized (int elements are zero initialized) (preferred)

    return 0;
}

If you provide more initializers than the array length, the compiler will error. If you provide fewer initializers, remaining elements are value initialized:

int main()
{
    int a[4] { 10, 20, 30, 40, 50 }; // compile error: too many initializers
    int b[4] { 10, 20 };              // b[2] and b[3] are value initialized

    return 0;
}

One limitation of C-style arrays is that you must explicitly specify the element type. CTAD doesn't work because C-style arrays aren't class templates, and using auto to deduce the element type from initializers also doesn't work:

int main()
{
    auto numbers[5] { 10, 20, 30, 40, 50 }; // compile error: can't use type deduction on C-style arrays

    return 0;
}

Omitted length

There's redundancy in this array definition:

int main()
{
    const int cubes[5] { 1, 8, 27, 64, 125 }; // cubes has length 5

    return 0;
}

We're telling the compiler the array has length 5, and we're also providing 5 initializers. When initializing with an initializer list, you can omit the length and let the compiler deduce it from the number of initializers:

int main()
{
    const int cubes1[5] { 1, 8, 27, 64, 125 }; // cubes1 explicitly defined to have length 5
    const int cubes2[] { 1, 8, 27, 64, 125 };  // cubes2 deduced by compiler to have length 5

    return 0;
}

This only works when providing initializers for all array members:

int main()
{
    int empty[] {}; // error: the compiler will deduce this to be a zero-length array, which is disallowed

    return 0;
}

When using an initializer list to initialize all elements, it's better to omit the length. This way, if you add or remove initializers, the length adjusts automatically without risk of mismatch.

Best Practice
Prefer omitting the length of a C-style array when explicitly initializing every array element with a value.

Const and constexpr C-style arrays

Like std::array, C-style arrays can be const or constexpr. Const arrays must be initialized, and element values cannot be changed afterward:

#include <iostream>

namespace Config
{
    constexpr int fibonacci[7] { 0, 1, 1, 2, 3, 5, 8 }; // an array of constexpr int
}

int main()
{
    const int cubes[5] { 1, 8, 27, 64, 125 }; // an array of const int
    cubes[0] = 10; // compile error: can't change const int

    return 0;
}

The sizeof a C-style array

The sizeof() operator returns the number of bytes used by the entire array:

#include <iostream>

int main()
{
    const int cubes[] { 1, 8, 27, 64, 125 }; // the compiler will deduce cubes to have length 5

    std::cout << sizeof(cubes); // prints 20 (assuming 4 byte ints)

    return 0;
}

Assuming 4-byte ints, this prints 20. The cubes array contains 5 int elements at 4 bytes each, so 5 * 4 = 20 bytes.

Note there's no overhead - a C-style array object contains only its elements.

Getting the length of a C-style array

In C++17, you can use std::size() (defined in the <iterator> header), which returns the array length as an unsigned integral value (std::size_t). In C++20, you can also use std::ssize(), which returns the array length as a signed integral value (probably std::ptrdiff_t):

#include <iostream>
#include <iterator> // for std::size and std::ssize

int main()
{
    const int cubes[] { 1, 8, 27, 64, 125 };  // the compiler will deduce cubes to have length 5

    std::cout << std::size(cubes) << '\n';  // C++17, returns unsigned integral value 5
    std::cout << std::ssize(cubes) << '\n'; // C++20, returns signed integral value 5

    return 0;
}
Tip
The canonical header for `std::size()` and `std::ssize()` is ``. However, many other headers also provide them (including `` and ``) because they're so useful. When using these functions on C-style arrays, you may need to explicitly include `` if you haven't included one of the other headers.

Getting the length of a C-style array (C++14 or older)

Prior to C++17, there was no standard library function to get C-style array length.

For C++11 or C++14, you can use this function:

#include <cstddef> // for std::size_t
#include <iostream>

template <typename T, std::size_t N>
constexpr std::size_t length(const T(&)[N]) noexcept
{
	return N;
}

int main() {

	int numbers[]{ 5, 10, 15, 20, 25, 30, 35 };
	std::cout << "The array has: " << length(numbers) << " elements\n";

	return 0;
}

This uses a function template that takes a C-style array by reference and returns the non-type template parameter representing the array's length.

In much older codebases, you may see array length determined by dividing the size of the entire array by the size of an array element:

#include <iostream>

int main()
{
    int numbers[12] {};
    std::cout << "The array has: " << sizeof(numbers) / sizeof(numbers[0]) << " elements\n";

    return 0;
}

This prints:

The array has: 12 elements

This works because: array size = length * element size, so rearranging: length = array size / element size. We typically use sizeof(numbers[0]) for element size, so length = sizeof(numbers) / sizeof(numbers[0]).

However, this formula can fail easily (when passed a decayed array), leaving the program unexpectedly broken. C++17's std::size() and the length() function template shown above will both cause compilation errors in this case, making them safe to use.

C-style arrays don't support assignment

C++ arrays don't support assignment:

int main()
{
    int data[] { 10, 20, 30 }; // okay: initialization is fine
    data[0] = 40;              // assignment to individual elements is fine
    data = { 50, 60, 70 };     // compile error: array assignment not valid

    return 0;
}

Technically, this doesn't work because assignment requires the left operand to be a modifiable lvalue, and C-style arrays are not considered modifiable lvalues.

If you need to assign a new list of values to an array, use std::vector instead. Alternatively, you can assign values element-by-element, or use std::copy to copy an existing C-style array:

#include <algorithm> // for std::copy

int main()
{
    int data[] { 10, 20, 30 };
    int source[] { 50, 60, 70 };

    // Copy source into data
    std::copy(std::begin(source), std::end(source), std::begin(data));

    return 0;
}

Summary

C-style arrays are built-in: Unlike std::array and std::vector which are library classes, C-style arrays are built directly into C++ and require no header inclusion. Standard library containers are typically implemented using C-style arrays.

Declaration syntax: Use square brackets with an optional length: type name[length]. The length must be a constant expression of type std::size_t and at least 1. Avoid variable-length arrays (VLAs) which are non-standard C++.

Subscripting: C-style arrays support operator[] for indexing. Unlike standard library containers, C-style array indices can be any integral type (signed or unsigned) or unscoped enumerations, avoiding sign conversion issues. No bounds checking is performed.

Aggregate initialization: Initialize with brace-enclosed lists. Too many initializers cause errors; too few value-initialize remaining elements. Empty braces value-initialize all elements (preferred over default initialization).

Omitted length: When providing initializers for all elements, omit the length and let the compiler deduce it. This prevents mismatches if you later add or remove initializers. Only works when initializing all elements.

No type deduction: C-style arrays don't support CTAD or auto type deduction for element types. You must explicitly specify the type.

Const and constexpr: C-style arrays can be const or constexpr just like std::array. Const arrays must be initialized and cannot be modified afterward.

Getting length: Use std::size() (C++17) or std::ssize() (C++20) from <iterator>. For older C++, use a custom template function. The sizeof(arr)/sizeof(arr[0]) hack is dangerous with decayed arrays.

No assignment: C-style arrays don't support assignment after initialization. You cannot assign a new list of values to an array. Use std::copy() or element-by-element assignment instead.

C-style arrays are the foundation but have limitations. Prefer std::array for constexpr arrays or std::vector for runtime arrays in modern C++.