# C-style arrays

Source: https://codewiki.com/cpp/c-arrays/

> - **what**: A C-style array is a fixed number of same-type objects laid out contiguously in memory. Its length is part of the array's type.
> - **trap**: An array converts to a pointer to its first element in most expressions and function parameters. The pointer carries no length, and indexing has no automatic bounds check.
> - **fix**: Prefer `std::array` for fixed-size value semantics, `std::span` for passing a contiguous sequence, and `std::vector` when the length changes at runtime.

## What it is and why it exists

A C-style array is a built-in compound type in C++. The declaration `int readings[4]` creates four `int` subobjects indexed from `0` through `3`, stored contiguously in memory. The `4` in that declaration is an array extent and part of the type: `int[4]` and `int[5]` are different types.

A built-in array provides the most direct layout for a fixed set of objects. It has no stored length field or separate control block. You meet it in structure members, string literals, C interfaces, and low-level library APIs. Its rules also explain problems that look like pointer problems, such as why a function does not receive an array's length and why a two-dimensional array cannot be passed as `int**`.

The type is part of the core language, but it has few operations. Arrays cannot be assigned as a whole, passed by value, or returned by value, and subscripting does not check bounds. Modern C++ code usually uses standard-library types to express ownership and interfaces, reserving built-in arrays for layout, interoperability, or small local fixed-size storage.

Three standard-library types cover the usual intentions. `std::array<T, N>` owns a fixed number of elements and has value semantics, `std::span` is a non-owning view of contiguous elements, and `std::vector` owns a runtime-sized sequence. They do not change the built-in rules, but they make length and ownership visible in an interface.

| Need | Usual type | Where the length lives |
|---|---|---|
| Own a fixed number of elements | `std::array<T, N>` | `N` in the type |
| Observe or modify contiguous elements | `std::span` | The span object or fixed-extent type |
| Grow and shrink at runtime | `std::vector` | The container object |
| Interoperate with an interface requiring built-in layout | `T[N]` or `T*` plus a count | The array type or a separate contract |

A character array is still just an array whose element type is `char`. `char name[] = "Ada"` has extent `4` because initialization also copies the terminating null character `'\0'`. Only a character array that follows the termination convention can be passed to an API expecting a C string; an ordinary character array does not automatically become a string.

## How it works

The declaration `T values[N]` creates `N` elements of type `T`. Standard C++ requires `N` to be greater than zero and to be a compile-time constant expression. Zero-length arrays and variable-length arrays accepted by GCC are extensions, not portable C++23. Use `std::vector` or another dynamic-storage type for a runtime length.

An array object is large enough to hold all its elements. For an index `i`, `values[i]` is defined in terms of `*(values + i)`: an array-to-pointer conversion happens first, followed by pointer arithmetic and dereferencing. A valid element index must be less than the extent.

An element pointer can move from the first element through the one-past-the-end position. The one-past pointer can delimit a half-open range, but it cannot be dereferenced. Pointer arithmetic is guaranteed only within one array object and its one-past position. Moving farther outside that range is not valid array traversal, even if the pointer is never dereferenced.

The initialization form determines the initial values. When an initializer list has fewer entries than the extent, the remaining elements are value-initialized, so `int counts[4]{2, 1}` produces `2, 1, 0, 0`. An automatic-storage array of fundamental type with no initializer contains indeterminate values; reading an element before assigning it has no usable semantics.

An array name is not a pointer variable. `sizeof values` operates on the whole array, `&values` has pointer-to-whole-array type, and `values` converts to `T*` only in most value-taking expressions. Consequently, `sizeof values / sizeof values[0]` works only in a scope where the array type has not been lost.

Array syntax in a function parameter is a particularly misleading exception. `void consume(int values[10])` is adjusted to `void consume(int* values)`; the `10` creates neither a runtime check nor part of the parameter type. If the function needs a length, pass it separately, accept an array reference, or, more commonly, accept `std::span`.

| Source spelling | Type information retained there |
|---|---|
| Local variable `int values[4]` | `int[4]` |
| `auto pointer = values` | `int*` |
| `auto& alias = values` | `int (&)[4]` |
| Parameter `void read(int values[4])` | Adjusted to `int*` |
| Parameter `void read(int (&values)[4])` | Reference to `int[4]` |

`std::span` stores a pointer and an extent, but it does not own the array or extend its lifetime. A dynamic-extent span normally stores the length at runtime; a fixed-extent `std::span<T, N>` puts `N` in the type. In C++23, `span::operator[]` still requires a valid index. It is not an automatically throwing safe subscript.

A multidimensional declaration is made from nested arrays. `int grid[2][3]` is an array of two elements, each of which is an `int[3]`. Its row subarrays are stored in sequence, giving row-major order; `grid[row][column]` selects a row first and then an element within that row.

An array object's lifetime follows its storage duration. An ordinary local array's lifetime ends when its scope exits, a static array lasts until program termination, and an array created by `new T[n]` lasts until its matching `delete[]`. A pointer or span obtained from a local array does not keep the elements alive after the function returns.

## Examples

### Preserve the extent while iterating

The first example uses a list initializer and lets `std::size` obtain the extent through an array reference. A range `for` also retains the array range, so the loop does not need a handwritten end index.

<!-- quick -->

```cpp
// file: readings.cpp
#include <iostream>
#include <iterator>

int main() {
    int readings[]{18, 21, 19, 24};

    std::cout << "count: " << std::size(readings) << '\n';

    for (int& reading : readings) {
        reading += 1;
    }

    std::cout << "adjusted:";
    for (int reading : readings) {
        std::cout << ' ' << reading;
    }
    std::cout << '\n';
}
```

```text
count: 4
adjusted: 19 22 20 25
```

<!-- /quick -->

The `int&` lets the first loop modify the array elements directly. The second loop reads each `int` by value. If you write `auto pointer = readings`, type deduction first triggers the array-to-pointer conversion; `auto& same_array = readings` preserves the `int[4]` type.

### Pass a pointer and length with span

The next example accepts a span. The caller can pass a built-in array, while the function receives both the first element and the element count instead of relying on a raw pointer parameter with a separate, easily desynchronized length.

```cpp
// file: span_total.cpp
#include <iostream>
#include <span>

int total(std::span<const int> values) {
    int result = 0;
    for (int value : values) {
        result += value;
    }
    return result;
}

void replace_negative(std::span<int> values) {
    for (int& value : values) {
        if (value < 0) {
            value = 0;
        }
    }
}

int main() {
    int balances[]{12, -3, 8, -1};
    replace_negative(balances);

    std::cout << "total: " << total(balances) << '\n';
    std::cout << "middle: " << total(std::span{balances}.subspan(1, 2)) << '\n';
}
```

```text
total: 20
middle: 8
```

`std::span<int>` lets `replace_negative` modify elements, whereas `std::span<const int>` provides read-only access. `subspan(1, 2)` describes two elements beginning at index `1`; the requested range still has to be valid when the subview is created.

### Deduce the extent through an array reference

A template can retain an extent through an array reference and deduce it as `N`. This interface works for a narrow utility that intentionally accepts only built-in arrays. A span is usually simpler when the function should also accept `std::array`, vectors, or subranges.

```cpp
// file: array_reference.cpp
#include <cstddef>
#include <iostream>

template<std::size_t N>
int last(const int (&values)[N]) {
    static_assert(N > 0);
    return values[N - 1];
}

template<std::size_t N>
void describe(const int (&values)[N]) {
    std::cout << "extent=" << N
              << ", first=" << values[0]
              << ", last=" << last(values) << '\n';
}

int main() {
    int ports[]{443, 8443, 9443};
    int retries[]{1, 2, 3, 5, 8};

    describe(ports);
    describe(retries);
}
```

```text
extent=3, first=443, last=9443
extent=5, first=1, last=8
```

The two calls instantiate different values of `N`, so `last` can access the final element without a second length parameter. That guarantee comes from the parameter type; it was not recovered from a pointer.

### Pass a two-dimensional array

The column extent of a two-dimensional array must be retained so the compiler can locate the next row. An array reference puts both row and column counts in the type and rejects an array with a different shape.

```cpp
// file: grid.cpp
#include <cstddef>
#include <iostream>

template<std::size_t Rows, std::size_t Cols>
void print_grid(const int (&grid)[Rows][Cols]) {
    std::cout << "shape: " << Rows << 'x' << Cols << '\n';

    for (std::size_t row = 0; row < Rows; ++row) {
        std::cout << "row " << row << ':';
        for (std::size_t column = 0; column < Cols; ++column) {
            std::cout << ' ' << grid[row][column];
        }
        std::cout << '\n';
    }
}

int main() {
    int seats[2][3]{
        {1, 0, 1},
        {0, 1, 1},
    };

    print_grid(seats);
}
```

```text
shape: 2x3
row 0: 1 0 1
row 1: 0 1 1
```

Here `grid[row]` is a complete `int[Cols]` row array before it converts to a pointer to that row's first element. If the function instead used the traditional parameter `const int grid[][3]`, it would adjust to a pointer to `int[3]` and still need the row count separately. It is never `int**`.

## Pitfalls

### Out-of-bounds access and one-past pointers

> **Pitfall:** A loop condition written as `index <= count` accesses the one-past position on its final iteration. Built-in arrays do not check bounds, so the access has undefined behavior. Looking correct in one test run is not evidence that the program is valid.

Use a half-open range: write `index < count`, or use a range `for`, a standard algorithm, or a span. Check `index < size` explicitly before subscripting with an externally supplied index. Test builds can enable AddressSanitizer and UndefinedBehaviorSanitizer, but a detector is not a substitute for carrying the bound in the interface.

### Deriving a length with sizeof inside a function

> **Pitfall:** In `void inspect(int values[8])`, `values` is a pointer. `sizeof(values) / sizeof(values[0])` divides the pointer size by the element size; it does not produce the caller's array extent.

Change the parameter to `std::span<const int>`, or pass a pointer and element count together at a low-level boundary. Use `const int (&values)[8]` only when the function must reject every other extent. In that spelling, the bracketed `8` really is part of the referred-to type.

### Assuming an array is copied as a value

> **Pitfall:** `auto backup = readings` deduces a pointer and copies no elements; `int backup[4] = readings` is ill-formed. An array is not an ordinary assignable value type.

Use `std::array` when you need an independent value, or copy elements into another existing range. If a built-in array is a class member, the class's implicit copy operation copies the member element by element. That is a rule for copying the enclosing class, not an array assignment operator.

### Returning a view of a local array

> **Pitfall:** Returning a pointer, reference, iterator, or span into a local array leaves a dangling view when the function returns. A span carries a length, but it neither owns elements nor extends their lifetime.

Make the owner outlive the view, or return an owning `std::array` or `std::vector` by value. When reviewing a helper, do not stop at the return type. Trace the storage duration of the array from which its span or pointer originated.

### Forgetting a character array's terminator

> **Pitfall:** `char code[3]{'A', 'B', 'C'}` contains three characters but is not a C string. Passing it to a function that expects null termination makes that function read beyond the array while searching for `\0`.

Reserve an element for the terminator, for example by letting `char code[] = "ABC"` deduce an extent of `4`. If the data permits embedded zero bytes or has no terminator, pass an explicit length or a suitable span instead of pretending that it is a C string.

<!-- deep -->

## Array types, extents, and conversions

An array's extent is part of its type, but a variable name rarely carries that fact very far. `decltype(values)` yields the declared array type, `sizeof values` measures the complete array object, and `std::size(values)` returns the element count through an array-reference overload. By contrast, ordinary by-value deduction in `auto value = values` produces a pointer to the first element.

Unary `&values` does not decay the array either. If `values` is `int[4]`, then `&values` is `int (*)[4]`, and adding `1` crosses the entire four-element array. `&values[0]` is an `int*`, so adding `1` advances by one element. The two pointers can represent the same starting address while retaining different types and arithmetic units.

Reference binding also preserves the array type. The template parameter `T (&array)[N]` can deduce both the element type and extent; `const T (&array)[N]` provides read-only access. This is useful when the extent itself determines behavior, but each distinct `N` produces a separate template instantiation. A span is often a better public interface.

The array-to-pointer conversion produces only the first element's address, not an end address. The resulting `T*` cannot distinguish one element, ten elements, or an address with no accessible element. Every raw-pointer interface therefore needs another contract defining a count, sentinel, or termination condition.

`std::span<T, N>` makes the representation choice explicit. The fixed extent `N` is part of the span type, whereas a dynamic-extent span exposes `size()` from the object. Both are views. Copying a span copies its observed location and length, not the underlying elements.

A built-in array cannot be a function's direct return type and has no array assignment. Putting an array in a structure changes how it can be used: the structure can be returned and assigned by value, and its generated operations process each array member. `std::array` packages value-style container operations as a standard-library type; which operations are available still depends on the element type.

An array of unknown bound is useful only in restricted declarations. For example, `extern int records[]` can declare an array defined elsewhere. Code cannot apply an operation requiring the complete size until it sees the full definition. This is not a runtime variable-length array, and the object does not hide a length field.

With `new T[n]`, `n` can be known at runtime, but the expression still returns `T*` with no queryable extent. Exceptions, early returns, and ownership transfer all make the matching `delete[]` harder to maintain. Unless you are implementing an owning container or adapting an interface that requires this shape, `std::vector` or an array smart pointer expresses the lifetime more clearly.

## Initialization, copying, and element lifetimes

Array initialization initializes each element in order; there is no separate construction step for an “array value.” A program is ill-formed if the initializer list has more elements than the extent. If it has fewer, every remaining element is initialized as if from an empty initializer list. That produces zero for `int` and runs the corresponding default construction for a class type.

| Declaration and location | Initial state of fundamental elements |
|---|---|
| Local `int data[4];` | Indeterminate; do not read before writing |
| Local `int data[4]{};` | All zero |
| Static-storage `int data[4];` | Zero-initialized during static initialization |
| `int data[4]{7, 8};` | `7, 8, 0, 0` |

Array elements are complete objects and follow the element type's construction and destruction rules. Class-type elements are constructed in increasing subscript order and destroyed in reverse order when the array lifetime ends. If construction of one element throws, earlier fully constructed elements are destroyed and later elements never begin their lifetimes.

Top-level const applies to the elements. The elements of `const int limits[3]{1, 2, 3}` cannot be changed through that array, and conversion produces `const int*`. Casting away const and writing does not make this valid. If the original object is actually const, the write has undefined behavior.

Read an array declarator outward from the variable name. `int* pointers[3]` is an array of three `int*` elements, while `int (*pointer)[3]` is a pointer to an array of three integers. Parentheses change the binding. For a complicated declaration, a type alias or standard-library container is usually clearer than another declarator layer.

A built-in array itself has no copy constructor or assignment operator. You cannot use an array name as the whole initializer for another array, and an array cannot appear on the left side of assignment. An element-wise algorithm, an enclosing class, or `std::array` supplies an explicit copying operation.

When an array is a class member, its lifetime is managed by the enclosing object. The outer object's default copy and move operations process the built-in array element by element, and destruction destroys every element. If an element is not copyable, the corresponding default copy operation of the outer class can be defined as deleted.

`typedef int Row[4]` or `using Row = int[4]` gives an array type a name. `Row table[3]` then means three rows of four integers each. An alias can make function references and row pointers easier to read, but it adds neither bounds checking nor value semantics.

Storage duration and ownership must be considered separately. A local array is normally owned by its scope, a static array by the program lifetime, and a member array by its enclosing object. Pointers, references, and spans only observe those elements. Their usability depends entirely on whether the owner still exists.

Arrays fit sets of objects whose extent is known at compile time. If the extent comes from input, putting it into a compiler-extension variable-length array on the stack introduces both portability and stack-space risks. A `std::vector` allocates dynamically and lets allocation failure follow the container's exception and RAII rules, which is usually easier to review.

## Expressing arrays at API boundaries

Array bugs often arise at a function boundary rather than at the storage declaration. A `T*` says only that `T` may be accessed through an address. It does not say whether the address may be null, how many elements exist, whether the call may modify them, or how long the pointer can be retained. A dependable interface states those constraints instead of making callers infer them from parameter names.

For an internal modern C++ interface, `std::span<const T>` commonly means a borrowed read-only sequence of contiguous elements, while `std::span` means a borrowed modifiable sequence. Const restricts writes through that view; it does not disable other aliases. The caller still has to keep the backing array alive and avoid replacing storage in a way that invalidates addresses while the span is used.

A fixed-extent `std::span<T, N>` works when an algorithm requires exactly `N` elements at compile time. It still owns nothing, but an array with the wrong extent cannot implicitly construct that span type. If the algorithm accepts any length, a dynamic-extent `std::span` reduces template instantiations and makes the interface broader.

A C-style interface usually uses a pointer plus count, such as `const unsigned char* data, std::size_t size`. Review the count's unit: an element count, a byte count, and a final valid index are different values. Using a byte count as the loop bound for `T*` crosses the real element range whenever `sizeof(T) > 1`.

Empty ranges also need a contract. A function must not dereference the data pointer when the length is zero; whether the interface permits a null pointer depends on its preconditions. A span provides one representation for an empty range and lets loops rely on `begin()` and `end()`, but constructing a span from an invalid pointer does not repair the pointer's origin.

Standard algorithms and range algorithms can accept an array while its extent is intact. `std::ranges::sort(values)` obtains the beginning and end from the array without a manual count. Compared with passing `values` and a repeated numeric constant, a range interface reduces opportunities for the pointer and length to disagree.

Use `std::to_array` or construct `std::array` explicitly when an existing built-in array should become an owning value. `std::to_array("ABC")` retains the string literal's null terminator, so the result has four elements. If the intent is text rather than raw character capacity, a `std::string` or `std::string_view` interface says so more directly.

ABI or C interoperability may require a raw-pointer shape. Do not pretend in the wrapper that the type now guarantees the bound. Validate the length and nullability, then form a span as early as possible for internal code. The wrapper should also state that ownership stays with the caller and whether a callback or asynchronous operation may retain the view beyond the call.

Generic code can inspect the number of array dimensions and each extent with `std::rank_v` and `std::extent_v<T, I>` from `<type_traits>`. These traits operate on types, so use them before decay, for example with `decltype(values)`. Once only an `int*` remains, a type trait cannot recover the original extent.

Interface selection has two axes: ownership and bounds. Use a container for ownership and copying, a span for borrowing with a count, and expose a raw pointer only where another contract already defines the count. This decision prevents more real errors than a stylistic preference between array and pointer syntax.

## Multidimensional and character-array bounds

Every layer of a multidimensional array has its own extent. For `int image[3][4]`, the outer array has three elements whose type is `int[4]`; `std::size(image)` is `3`, and `std::size(image[0])` is `4`. Both the row and column index need their own bounds proof.

When `image` appears in most expressions, it converts to `int (*)[4]`, not `int*`. The compiler needs the four-element row type so that `image + 1` can advance to the next row. The parameter `int image[][4]` adjusts to the same pointer type, which is why only the outermost extent can be omitted.

`int**` is a pointer to `int*`. A common layout behind it is an array of row pointers whose rows may live in separate allocations. A two-dimensional array has no such row-pointer objects; it directly contains row arrays. A cast cannot create the missing pointers, and dereferencing the array as `int**` misinterprets element bytes.

Nested arrays store rows in outer-element order and columns in each row's element order, naturally producing row-major layout. Row-by-row and column-by-column access stays within the nested object bounds. Treating `&image[0][0]` as a one-dimensional pointer on which arbitrary arithmetic crosses all rows instead ignores the subarray boundaries. If flat storage is required, declare a one-dimensional array and compute `row * columns + column` explicitly.

Partial list initialization fills with zero at every layer. `int image[2][3]{{1}, {2, 3}}` produces rows `1, 0, 0` and `2, 3, 0`. Inner braces can be omitted for some simple scalar arrays, but keeping the layers visible better communicates the shape and gives the compiler more opportunity to warn about suspicious initialization.

A string literal is itself a `const char[N]` array, where `N` includes the terminating null character. Initializing `char text[]` from it creates a modifiable array copy. Making `const char* text` point to the literal creates only a pointer, and modifying the literal still has undefined behavior. `sizeof` also differs: the former gives the array capacity, while the latter gives the pointer size.

Character-array capacity, current text length, and binary-data length are separate concepts. `sizeof buffer` gives the capacity only while `buffer` retains array type. `std::strlen(buffer)` requires a reachable `\0` and counts only the characters before it. Data that may contain zero bytes needs a separately carried length; a span is more suitable than the string-termination convention.

A string literal can contain an embedded `\0`, so its array extent need not equal `std::strlen` plus one. `char sample[] = "A\0B"` has extent four, while C string functions see a text length of one. For protocol fields or file content, do not replace an explicit data length with null-terminated string rules.

Before ordering or subtracting two element pointers, confirm that they belong to the same array object or its corresponding one-past position. Numerically adjacent addresses do not create that relationship. Ordering or subtracting pointers from different arrays cannot establish one larger contiguous range.

An array member does not turn into a pointer transfer merely because its enclosing object is moved. A default move of the outer object applies move initialization or move assignment to each array element. For a large fixed collection, the mere fact that a move occurred does not guarantee constant cost.

A bounds review ultimately answers three questions together: what the complete type is, whether the current expression has already decayed, and how long the backing object remains alive. Checking only a subscript's numeric value is insufficient. A correct number paired with the wrong row type or a dangling span still produces undefined behavior.

<!-- /deep -->

[Checkpoint: cpp/c-arrays](https://codewiki.com/cpp/c-arrays/#checkpoint)

## Further reading

- [C++23 working draft: arrays](https://timsong-cpp.github.io/cppwp/n4950/dcl.array)
- [C++23 working draft: array-to-pointer conversion](https://timsong-cpp.github.io/cppwp/n4950/conv.array)
- [cppreference: array declaration](https://en.cppreference.com/w/cpp/language/array)
- [cppreference: `std::span`](https://en.cppreference.com/w/cpp/container/span)
- [C++ Core Guidelines: bounds safety](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-bounds-array)
