CS 70

Declaring Arrays in C++

Declaring an array of values looks a lot like declaring a single value, except that we add the size of the array in square brackets after the name:

int values[42];
  • Goat speaking

    How do I pronounce that line? “int values square-brackets 42”?

  • LHS Cow speaking

    We read this declaration using the “inside-out” rule: start at the variable name, then move out toward the edges. Go right first, then left.

  • RHS Cow speaking

    In this case, we'd say, “Values is an array of forty-two ints”.

  • LHS Cow speaking

    We'll talk more about reading types a bit later in the semester.

Arrays of primitives (basic building block types, like numbers) in C++ follow the same object lifetime as single primitive variable.

  • For allocation and deallocation, the only difference is that an array of n values will need to manage n times as much space!

  • When it comes to initialization and destruction, they're initialized in order (first element first) and destroyed in reverse order (first element last).

For example, here is a little program:

int main() {
    int num = 42;
    int arr[5];

    // Probably we should do something with these variables, but never mind.

    return 0;
}

Stack Frame Slots

Just hazard an educated guess: in a memory diagram of this program, how many slots do we need in main's stack frame?

memory diagram of an array

You can see that the array takes up five slots, one for each int in the array.

We've left the names for the individual slots off for now. We'll get to that in a moment.

Allocation for Arrays of Primitives

At the opening curly brace of the function, space will be allocated in a contiguous block for all of the values in the array.

If our function has the declaration

int myArray[3];

the compiler will write the instruction(s) needed to allocate enough space for three ints.

  • Cat speaking

    So this is figured out before the program even runs?

  • LHS Cow speaking

    Yes. Remember that it's the compiler's job to write the instruction(s) saying how much memory to allocate to hold the array.

  • RHS Cow speaking

    So we need to know how big the array will need to be at compile time.

The compiler always needs to know when it compiles the code how big an array is going to be so it can set aside the right fixed amount of space, so writing code like the following is not allowed:

int values[];   // This code won't compile, the size is missing.

Our compiler, clang, produces an error like

arrayBadDecl1.cpp:2:9: error: definition of variable with array type needs an explicit size or an initializer
    int values[];
        ^
1 error generated.
  • Duck speaking

    I'm thinking about when you showed the size, with int myArray[3];… Didn't we learn in previous classes that sticking random “magic numbers” in our code was bad?

  • LHS Cow speaking

    Yes—a “magic number” is a raw numeric value in our code.

  • RHS Cow speaking

    We do want to avoid magic numbers, because they are bad for maintainability.

  • LHS Cow speaking

    If you need to change the number, you'll need to change it everywhere it appears and could make mistakes while doing so.

  • Duck speaking

    So… can we use a variable instead of a magic number to say how big our array is?

  • LHS Cow speaking

    Let's look at that… there is actually a reason we can't use a variable there.

You might be tempted to write something like

// The compiler won't like this code either...
size_t numberOfValues = 3;
int myArray[numberOfValues];

But it won't compile. One popular C++ compiler (g++) produces the following error for this code:

arrayBadDecl2.cpp: In function 'int main()':
arrayBadDecl2.cpp:5:9: warning: ISO C++ forbids variable length array 'myArray' [-Wvla]
    5 |     int myArray[numberOfValues];
      |         ^~~~~~~

Remember that we said that memory is set aside for all function variables before the code starts running (“buying the land” in our building analogy). As a result, the compiler expects to figure this stuff out when it is compiling the code (i.e., “at compile time”), and generate the necessary machine instructions to allocate this fixed-size space on the stack frame.

You might think that the compiler could see that we put the value 3 in numberOfValues, but assigning values to variables is something that happens when the program runs, not when it is compiled. As human beings, we might be able to reason out that the value we just put in that variable will still be there at the very next line of code where we used it, but the compiler shouldn't be expected to figure out that kind of stuff. At compile time, the compiler is mostly paying attention to variable types, not what values they will have.

So with this example, all the compiler knows for sure is that we need space for numberOfValues integers!

And it doesn't know at compile time what the value of numberOfValues is, since that won't be set until the program actually runs.

  • Cat speaking

    But I'm sure there has to be some way to avoid magic numbers?

  • LHS Cow speaking

    We can make this work. We just have to change numberOfValues so it isn't a variable but a constant.

The constexpr keyword tells the compiler that

  • The value of this variable should be known at compile time (so, if it isn't, generate an error) and
  • The value of variable should not change during runtime (so, if it does, generate an error).

As a result, variables that are constexpr are okay to use as array sizes.

constexpr size_t numberOfValues = 3; // Constant value, known at compile time.
int myArray[numberOfValues];         // Which makes this code okay.

Now that numberOfValues is a compile-time constant expression, it is reasonable to expect the compiler to figure out what value it might have at some arbitrary point in the code, because it always holds the exact same fixed value.

Initialization for Arrays of Primitives

At the declaring line, all of the values in the array will be initialized, if they've been given. We can give initial values for an array by putting them in a comma-separated list inside curly braces; so

int oneTwoThree[3] = {1,2,3};
int fourFiveSix[3]{4,5,6}; // also works

will initialize the first value in oneTwoThree to be 1, the second to be 2, and the third to be 3.

  • Cat speaking

    That makes sense for three values… but won't it get kind of cumbersome if we have an array with a thousand values in it?

  • LHS Cow speaking

    Yes! We have the option of leaving off the initial values.

If we write

int oneTwoThree[3];

then oneTwoThree will still be initialized on the declaration line, but since we didn't give initial values, the ints will all be default initialized.

  • Dog speaking

    Default initialized means set to zero, right?

  • LHS Cow speaking

    No—for primitive types like int and double, they have indeterminate value when default initialized. Don't expect them to hold any kind of sensible value.

  • RHS Cow speaking

    Frequently it's “whatever bits happened to be lying around when the space was allocated on the stack.”

In most situations, it's considered bad practice to default initialize variables that hold primitive types (e.g., int, double, etc.), but it's okay to default initialize the values in an array of primitive types as long as the very next thing you do is set the values (e.g., using a for loop).

We'll see how to do that on the next page!

(When logged in, completion status appears here.)