C++ inherited arrays from C where they are used virtually everywhere. C++ provides abstractions that are easier to use and less error-prone (std::vector<T>
since C++98 and std::array<T, n>
since C++11), so the need for arrays does not arise quite as often as it does in C. However, when you read legacy code or interact with a library written in C, you should have a firm grasp on how arrays work.
This FAQ is split into five parts:
- arrays on the type level and accessing elements
- array creation and initialization
- assignment and parameter passing
- multidimensional arrays and arrays of pointers
- common pitfalls when using arrays
If you feel something important is missing in this FAQ, write an answer and link it here as an additional part.
In the following text, "array" means "C array", not the class template std::array
. Basic knowledge of the C declarator syntax is assumed. Note that the manual usage of new
and delete
as demonstrated below is extremely dangerous in the face of exceptions, but that is the topic of another FAQ.
(Note: This is meant to be an entry to Stack Overflow's C++ FAQ. If you want to critique the idea of providing an FAQ in this form, then the posting on meta that started all this would be the place to do that. Answers to that question are monitored in the C++ chatroom, where the FAQ idea started out in the first place, so your answer is very likely to get read by those who came up with the idea.)
5. Common pitfalls when using arrays.
5.1 Pitfall: Trusting type-unsafe linking.
OK, you’ve been told, or have found out yourself, that globals (namespace scope variables that can be accessed outside the translation unit) are Evil™. But did you know how truly Evil™ they are? Consider the program below, consisting of two files [main.cpp] and [numbers.cpp]:
In Windows 7 this compiles and links fine with both MinGW g++ 4.4.1 and Visual C++ 10.0.
Since the types don't match, the program crashes when you run it.
In-the-formal explanation: the program has Undefined Behavior (UB), and instead of crashing it can therefore just hang, or perhaps do nothing, or it can send threating e-mails to the presidents of the USA, Russia, India, China and Switzerland, and make Nasal Daemons fly out of your nose.
In-practice explanation: in
main.cpp
the array is treated as a pointer, placed at the same address as the array. For 32-bit executable this means that the firstint
value in the array, is treated as a pointer. I.e., inmain.cpp
thenumbers
variable contains, or appears to contain,(int*)1
. This causes the program to access memory down at very bottom of the address space, which is conventionally reserved and trap-causing. Result: you get a crash.The compilers are fully within their rights to not diagnose this error, because C++11 §3.5/10 says, about the requirement of compatible types for the declarations,
The same paragraph details the variation that is allowed:
This allowed variation does not include declaring a name as an array in one translation unit, and as a pointer in another translation unit.
5.2 Pitfall: Doing premature optimization (
memset
& friends).Not written yet
5.3 Pitfall: Using the C idiom to get number of elements.
With deep C experience it’s natural to write …
Since an
array
decays to pointer to first element where needed, the expressionsizeof(a)/sizeof(a[0])
can also be written assizeof(a)/sizeof(*a)
. It means the same, and no matter how it’s written it is the C idiom for finding the number elements of array.Main pitfall: the C idiom is not typesafe. For example, the code …
passes a pointer to
N_ITEMS
, and therefore most likely produces a wrong result. Compiled as a 32-bit executable in Windows 7 it produces …int const a[7]
to justint const a[]
.int const a[]
toint const* a
.N_ITEMS
is therefore invoked with a pointer.sizeof(array)
(size of a pointer) is then 4.sizeof(*array)
is equivalent tosizeof(int)
, which for a 32-bit executable is also 4.In order to detect this error at run time you can do …
The runtime error detection is better than no detection, but it wastes a little processor time, and perhaps much more programmer time. Better with detection at compile time! And if you're happy to not support arrays of local types with C++98, then you can do that:
Compiling this definition substituted into the first complete program, with g++, I got …
How it works: the array is passed by reference to
n_items
, and so it does not decay to pointer to first element, and the function can just return the number of elements specified by the type.With C++11 you can use this also for arrays of local type, and it's the type safe C++ idiom for finding the number of elements of an array.
5.4 C++11 & C++14 pitfall: Using a
constexpr
array size function.With C++11 and later it's natural, but as you'll see dangerous!, to replace the C++03 function
with
where the significant change is the use of
constexpr
, which allows this function to produce a compile time constant.For example, in contrast to the C++03 function, such a compile time constant can be used to declare an array of the same size as another:
But consider this code using the
constexpr
version:The pitfall: as of July 2015 the above compiles with MinGW-64 5.1.0 with
C++11 C++14 $5.19/2 nineth dash-pedantic-errors
, and, testing with the online compilers at gcc.godbolt.org/, also with clang 3.0 and clang 3.2, but not with clang 3.3, 3.4.1, 3.5.0, 3.5.1, 3.6 (rc1) or 3.7 (experimental). And important for the Windows platform, it does not compile with Visual C++ 2015. The reason is a C++11/C++14 statement about use of references inconstexpr
expressions:One can always write the more verbose
… but this fails when
Collection
is not a raw array.To deal with collections that can be non-arrays one needs the overloadability of an
n_items
function, but also, for compile time use one needs a compile time representation of the array size. And the classic C++03 solution, which works fine also in C++11 and C++14, is to let the function report its result not as a value but via its function result type. For example like this:About the choice of return type for
static_n_items
: this code doesn't usestd::integral_constant
because withstd::integral_constant
the result is represented directly as aconstexpr
value, reintroducing the original problem. Instead of aSize_carrier
class one can let the function directly return a reference to an array. However, not everybody is familiar with that syntax.About the naming: part of this solution to the
constexpr
-invalid-due-to-reference problem is to make the choice of compile time constant explicit.Hopefully the oops-there-was-a-reference-involved-in-your-
constexpr
issue will be fixed with C++17, but until then a macro like theSTATIC_N_ITEMS
above yields portability, e.g. to the clang and Visual C++ compilers, retaining type safety.Related: macros do not respect scopes, so to avoid name collisions it can be a good idea to use a name prefix, e.g.
MYLIB_STATIC_N_ITEMS
.Array creation and initialization
As with any other kind of C++ object, arrays can be stored either directly in named variables (then the size must be a compile-time constant; C++ does not support VLAs), or they can be stored anonymously on the heap and accessed indirectly via pointers (only then can the size be computed at runtime).
Automatic arrays
Automatic arrays (arrays living "on the stack") are created each time the flow of control passes through the definition of a non-static local array variable:
Initialization is performed in ascending order. Note that the initial values depend on the element type
T
:T
is a POD (likeint
in the above example), no initialization takes place.T
initializes all the elements.T
provides no accessible default-constructor, the program does not compile.Alternatively, the initial values can be explicitly specified in the array initializer, a comma-separated list surrounded by curly brackets:
Since in this case the number of elements in the array initializer is equal to the size of the array, specifying the size manually is redundant. It can automatically be deduced by the compiler:
It is also possible to specify the size and provide a shorter array initializer:
In that case, the remaining elements are zero-initialized. Note that C++ allows an empty array initializer (all elements are zero-initialized), whereas C89 does not (at least one value is required). Also note that array initializers can only be used to initialize arrays; they cannot later be used in assignments.
Static arrays
Static arrays (arrays living "in the data segment") are local array variables defined with the
static
keyword and array variables at namespace scope ("global variables"):(Note that variables at namespace scope are implicitly static. Adding the
static
keyword to their definition has a completely different, deprecated meaning.)Here is how static arrays behave differently from automatic arrays:
(None of the above is specific to arrays. These rules apply equally well to other kinds of static objects.)
Array data members
Array data members are created when their owning object is created. Unfortunately, C++03 provides no means to initialize arrays in the member initializer list, so initialization must be faked with assignments:
Alternatively, you can define an automatic array in the constructor body and copy the elements over:
In C++0x, arrays can be initialized in the member initializer list thanks to uniform initialization:
This is the only solution that works with element types that have no default constructor.
Dynamic arrays
Dynamic arrays have no names, hence the only means of accessing them is via pointers. Because they have no names, I will refer to them as "anonymous arrays" from now on.
In C, anonymous arrays are created via
malloc
and friends. In C++, anonymous arrays are created using thenew T[size]
syntax which returns a pointer to the first element of an anonymous array:The following ASCII art depicts the memory layout if the size is computed as 8 at runtime:
Obviously, anonymous arrays require more memory than named arrays due to the extra pointer that must be stored separately. (There is also some additional overhead on the free store.)
Note that there is no array-to-pointer decay going on here. Although evaluating
new int[size]
does in fact create an array of integers, the result of the expressionnew int[size]
is already a pointer to a single integer (the first element), not an array of integers or a pointer to an array of integers of unknown size. That would be impossible, because the static type system requires array sizes to be compile-time constants. (Hence, I did not annotate the anonymous array with static type information in the picture.)Concerning default values for elements, anonymous arrays behave similar to automatic arrays. Normally, anonymous POD arrays are not initialized, but there is a special syntax that triggers value-initialization:
(Note the trailing pair of parenthesis right before the semicolon.) Again, C++0x simplifies the rules and allows specifying initial values for anonymous arrays thanks to uniform initialization:
If you are done using an anonymous array, you have to release it back to the system:
You must release each anonymous array exactly once and then never touch it again afterwards. Not releasing it at all results in a memory leak (or more generally, depending on the element type, a resource leak), and trying to release it multiple times results in undefined behavior. Using the non-array form
delete
(orfree
) instead ofdelete[]
to release the array is also undefined behavior.Arrays on the type level
An array type is denoted as
T[n]
whereT
is the element type andn
is a positive size, the number of elements in the array. The array type is a product type of the element type and the size. If one or both of those ingredients differ, you get a distinct type:Note that the size is part of the type, that is, array types of different size are incompatible types that have absolutely nothing to do with each other.
sizeof(T[n])
is equivalent ton * sizeof(T)
.Array-to-pointer decay
The only "connection" between
T[n]
andT[m]
is that both types can implicitly be converted toT*
, and the result of this conversion is a pointer to the first element of the array. That is, anywhere aT*
is required, you can provide aT[n]
, and the compiler will silently provide that pointer:This conversion is known as "array-to-pointer decay", and it is a major source of confusion. The size of the array is lost in this process, since it is no longer part of the type (
T*
). Pro: Forgetting the size of an array on the type level allows a pointer to point to the first element of an array of any size. Con: Given a pointer to the first (or any other) element of an array, there is no way to detect how large that array is or where exactly the pointer points to relative to the bounds of the array. Pointers are extremely stupid.Arrays are not pointers
The compiler will silently generate a pointer to the first element of an array whenever it is deemed useful, that is, whenever an operation would fail on an array but succeed on a pointer. This conversion from array to pointer is trivial, since the resulting pointer value is simply the address of the array. Note that the pointer is not stored as part of the array itself (or anywhere else in memory). An array is not a pointer.
One important context in which an array does not decay into a pointer to its first element is when the
&
operator is applied to it. In that case, the&
operator yields a pointer to the entire array, not just a pointer to its first element. Although in that case the values (the addresses) are the same, a pointer to the first element of an array and a pointer to the entire array are completely distinct types:The following ASCII art explains this distinction:
Note how the pointer to the first element only points to a single integer (depicted as a small box), whereas the pointer to the entire array points to an array of 8 integers (depicted as a large box).
The same situation arises in classes and is maybe more obvious. A pointer to an object and a pointer to its first data member have the same value (the same address), yet they are completely distinct types.
If you are unfamiliar with the C declarator syntax, the parenthesis in the type
int(*)[8]
are essential:int(*)[8]
is a pointer to an array of 8 integers.int*[8]
is an array of 8 pointers, each element of typeint*
.Accessing elements
C++ provides two syntactic variations to access individual elements of an array. Neither of them is superior to the other, and you should familiarize yourself with both.
Pointer arithmetic
Given a pointer
p
to the first element of an array, the expressionp+i
yields a pointer to the i-th element of the array. By dereferencing that pointer afterwards, one can access individual elements:If
x
denotes an array, then array-to-pointer decay will kick in, because adding an array and an integer is meaningless (there is no plus operation on arrays), but adding a pointer and an integer makes sense:(Note that the implicitly generated pointer has no name, so I wrote
x+0
in order to identify it.)If, on the other hand,
x
denotes a pointer to the first (or any other) element of an array, then array-to-pointer decay is not necessary, because the pointer on whichi
is going to be added already exists:Note that in the depicted case,
x
is a pointer variable (discernible by the small box next tox
), but it could just as well be the result of a function returning a pointer (or any other expression of typeT*
).Indexing operator
Since the syntax
*(x+i)
is a bit clumsy, C++ provides the alternative syntaxx[i]
:Due to the fact that addition is commutative, the following code does exactly the same:
The definition of the indexing operator leads to the following interesting equivalence:
However,
&x[0]
is generally not equivalent tox
. The former is a pointer, the latter an array. Only when the context triggers array-to-pointer decay canx
and&x[0]
be used interchangeably. For example:On the first line, the compiler detects an assignment from a pointer to a pointer, which trivially succeeds. On the second line, it detects an assignment from an array to a pointer. Since this is meaningless (but pointer to pointer assignment makes sense), array-to-pointer decay kicks in as usual.
Ranges
An array of type
T[n]
hasn
elements, indexed from0
ton-1
; there is no elementn
. And yet, to support half-open ranges (where the beginning is inclusive and the end is exclusive), C++ allows the computation of a pointer to the (non-existent) n-th element, but it is illegal to dereference that pointer:For example, if you want to sort an array, both of the following would work equally well:
Note that it is illegal to provide
&x[n]
as the second argument since this is equivalent to&*(x+n)
, and the sub-expression*(x+n)
technically invokes undefined behavior in C++ (but not in C99).Also note that you could simply provide
x
as the first argument. That is a little too terse for my taste, and it also makes template argument deduction a bit harder for the compiler, because in that case the first argument is an array but the second argument is a pointer. (Again, array-to-pointer decay kicks in.)Programmers often confuse multidimensional arrays with arrays of pointers.
Multidimensional arrays
Most programmers are familiar with named multidimensional arrays, but many are unaware of the fact that multidimensional array can also be created anonymously. Multidimensional arrays are often referred to as "arrays of arrays" or "true multidimensional arrays".
Named multidimensional arrays
When using named multidimensional arrays, all dimensions must be known at compile time:
This is how a named multidimensional array looks like in memory:
Note that 2D grids such as the above are merely helpful visualizations. From the point of view of C++, memory is a "flat" sequence of bytes. The elements of a multidimensional array are stored in row-major order. That is,
connect_four[0][6]
andconnect_four[1][0]
are neighbors in memory. In fact,connect_four[0][7]
andconnect_four[1][0]
denote the same element! This means that you can take multi-dimensional arrays and treat them as large, one-dimensional arrays:Anonymous multidimensional arrays
With anonymous multidimensional arrays, all dimensions except the first must be known at compile time:
This is how an anonymous multidimensional array looks like in memory:
Note that the array itself is still allocated as a single block in memory.
Arrays of pointers
You can overcome the restriction of fixed width by introducing another level of indirection.
Named arrays of pointers
Here is a named array of five pointers which are initialized with anonymous arrays of different lengths:
And here is how it looks like in memory:
Since each line is allocated individually now, viewing 2D arrays as 1D arrays does not work anymore.
Anonymous arrays of pointers
Here is an anonymous array of 5 (or any other number of) pointers which are initialized with anonymous arrays of different lengths:
And here is how it looks like in memory:
Conversions
Array-to-pointer decay naturally extends to arrays of arrays and arrays of pointers:
However, there is no implicit conversion from
T[h][w]
toT**
. If such an implicit conversion did exist, the result would be a pointer to the first element of an array ofh
pointers toT
(each pointing to the first element of a line in the original 2D array), but that pointer array does not exist anywhere in memory yet. If you want such a conversion, you must create and fill the required pointer array manually:Note that this generates a view of the original multidimensional array. If you need a copy instead, you must create extra arrays and copy the data yourself:
Assignment
For no particular reason, arrays cannot be assigned to one another. Use
std::copy
instead:This is more flexible than what true array assignment could provide because it is possible to copy slices of larger arrays into smaller arrays.
std::copy
is usually specialized for primitive types to give maximum performance. It is unlikely thatstd::memcpy
performs better. If in doubt, measure.Although you cannot assign arrays directly, you can assign structs and classes which contain array members. That is because array members are copied memberwise by the assignment operator which is provided as a default by the compiler. If you define the assignment operator manually for your own struct or class types, you must fall back to manual copying for the array members.
Parameter passing
Arrays cannot be passed by value. You can either pass them by pointer or by reference.
Pass by pointer
Since arrays themselves cannot be passed by value, usually a pointer to their first element is passed by value instead. This is often called "pass by pointer". Since the size of the array is not retrievable via that pointer, you have to pass a second parameter indicating the size of the array (the classic C solution) or a second pointer pointing after the last element of the array (the C++ iterator solution):
As a syntactic alternative, you can also declare parameters as
T p[]
, and it means the exact same thing asT* p
in the context of parameter lists only:You can think of the compiler as rewriting
T p[]
toT *p
in the context of parameter lists only. This special rule is partly responsible for the whole confusion about arrays and pointers. In every other context, declaring something as an array or as a pointer makes a huge difference.Unfortunately, you can also provide a size in an array parameter which is silently ignored by the compiler. That is, the following three signatures are exactly equivalent, as indicated by the compiler errors:
Pass by reference
Arrays can also be passed by reference:
In this case, the array size is significant. Since writing a function that only accepts arrays of exactly 8 elements is of little use, programmers usually write such functions as templates:
Note that you can only call such a function template with an actual array of integers, not with a pointer to an integer. The size of the array is automatically inferred, and for every size
n
, a different function is instantiated from the template. You can also write quite useful function templates that abstract from both the element type and from the size.