7.3. Creating Arrays

Time: 00:02:56 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)

The Introduction to Pointers in chapter 4 warned that it was necessary to refer to arrays before formally introducing them. This section demonstrates why the forward reference was necessary and builds on the pointer concepts introduced in Chapter 4. Specifically, chapter 4 indicated that C++ could create data as automatic variables on the runtime stack or as dynamic variables on the heap using the new operator. The ability of C++ to allocate memory from either the stack or the heap also applies to arrays. But perhaps the most surprising fact awaiting us is the strong relationship between arrays and pointers, specifically that the array's name is a pointer.

int	scores[50];
int*	p2 = scores;
...scores[10]...
...p2[5]...
void function(int* array) { . . .}	// definition
function(scores);			// call
(a)(b)(c)
The name of an array is an address. Whenever the name of an array appears in an expression (as opposed to a definition), that name represents the address of the array. That is, the name of an array is a constant pointer. Note that the ellipses in the examples represent code omitted for simplicity.
  1. The name of an array is an address, so it is assignment compatible with a pointer variable. Understanding that the assignment operation does not copy the array is crucial. Assignment creates two pointer variables, scores and p2 that point to a single array - that is, the assignment creates an alias p2 for the scores array (see Copying Arrays) - scores is constant while p2 may change.
  2. The square brackets, [ and ], form the indexing operator that locates a specific element within an array. The indexing operator operates on a memory address. Therefore, in this example, programs can use it with both scores and p2. The compiler locates each element in an array by calculating an offset from the beginning address represented by the array's name using simple address arithmetic. For example, scores is an int array, so the compiler translates scores[i] into the simple address calculation scores + i * sizeof(int).
  3. C++ programs always pass arrays to functions by pointer. However, an array name is an address, making the address of operator, &, unnecessary.

Automatic and Dynamic Arrays

Chapter 1 introduced us to automatic and static variables. Computers allocate and deallocate memory for automatic variables as they come into and go out of scope. In contrast, they allocate the memory for static variables when they load the program into memory and only deallocate it when it ends. We also learned in Chapter 1 that variables are automatic by default - which means that all the arrays seen so far in this chapter are automatic - but that we can override this default behavior with the static keyword.

In Chapter 4, we learned about the runtime stack and the heap. We learned that the computer allocates memory for automatic variables from the stack, while it allocates memory for dynamic variables with the new operator from the heap. As an array is, in part, just a variable, all these concepts and behaviors apply to arrays.

int scores1[50];
The array scores1 is single entity allocated as a contiguous sequence of integers on the stack.
(a)
 
int* scores2 = new int[50];
Creating the array scores2 on the heap requires two entities. The first is a contiguous sequence of integers allocated on the heap. The new operator returns the address of the beginning of the sequence. The second entity is the pointer variable scores2. The assignment operation stores the sequence's beginning address in the pointer.
(b)
Allocating memory for an array.
  1. scores1 is an automatic array, so the program allocates its memory on the stack and automatically deallocates it when scores1 goes out of scope. This behavior makes managing automatic or local arrays quite simple but can also be the source of a tricky bug identified later in this chapter.
  2. Dynamic arrays often consist of two parts. First, the new operator allocates a memory block on the heap and returns its starting address. Second, the program typically stores the address in a pointer variable, scores2 in the example, that names the array. The pointer might also be on the heap but is often on the stack, as illustrated. The program will create a "memory leak" (i.e., the heap memory is lost and becomes unavailable to the program) if it fails to deallocate the heap memory before scores2 goes out of scope.

    Memory allocated with the new operator is deallocated and returned to the heap with the delete operator. When a program deallocates an array, square brackets signal the deallocator that an array, rather than a single object, is being deallocated. So, the statement to deallocate the scores2 array is:

    delete[] scores2;

If the variable definition of (a) is changed to static int scores1[50];, the resulting picture is similar to (a). However, the program doesn't allocate the array's memory on the stack and only deallocates it once the program ends.

Automatic variables, including automatic arrays, are generally easier to work with and slightly faster than dynamic variables. However, automatic variables are much less flexible than dynamic variables. In many programs, the flexibility that dynamic variables offer over automatic variables is sufficient to outweigh the increased complexity and the slight performance penalty that they incur.

Specifying Array Sizes

When working with arrays, it's important to remember that the notation [50] can mean two different things depending on where programmers use it. If they use it as part of the array definition, then [50] represents the size of one of the array's dimensions. Used anywhere else, it refers to one specific element in the array. The previous array examples use a constant value between the square brackets for both operations. Can a variable ever be used instead of a constant?

Programs frequently use variables for array indexes but are restricted when they can use them for array sizes. As Figure 2 illustrates, C++ provides two ways to create an array: automatically or dynamically. Dynamically creating an array with the new operator involves pointers and, aside from the asterisk, is the same as it would appear in a Java program. The dynamic technique has the advantage that programmers can replace the value "50" with a variable. Unfortunately, they can't use a variable when defining an array as an automatic or local variable.

When we create an array as an automatic or local variable, we must specify the array size with a compile-time constant. A compile-time constant has two characteristics: First, a programmer or the preprocessor sets its value by the time that the compiler component (the middle component of the complete compiler system) generates the code that allocates memory for the array. And second, the value of the constant cannot change from that point in time onward. The following list illustrates four ways of specifying a compile-time constant; the first three are as symbolic a constant:

  1. as a numeric literal: 100
  2. as a macro: #define SIZE 100
  3. as an enum: enum { SIZE = 100 };
  4. using the const keyword: const int SIZE = 100;

Creating arrays dynamically with the new operator is more flexible than creating them automatically. Programmers must know the size of an automatic array at compile time. However, they can delay specifying the size of a dynamic array until they create it. Although programmers can specify the size of a dynamic array with any of the four techniques outlined above, they can also wait to specify the size until the program runs. A running program can allow a user to enter the size of a dynamic array or calculate the size using program variables.

Automatic/Compile-Time AllocationDynamic/Runtime Allocation
const int SIZE = 100;	// compile-time constant


int	test[SIZE];	// automatic array


if (student >= 0 && student < SIZE)
	. . . test[student] . . .


for (int i = 0; i < SIZE; i++)
	. . . test[i] << . . .
int	size;

cout << "Please enter the size: ";
cin >> size;;

int*	test = new int[size;];	// dynamic array

if (student >= 0 && student < size)
	. . . test[student] . . .

for (int i = 0; i < size;; i++)
	. . . test[i] << . . .
(a)(b)
Arrays created automatically and dynamically. The code fragments illustrate how to create automatic and dynamic arrays. Note that student is an integer variable defined and initialized elsewhere.
  1. Demonstrates using a symbolic constant to specify an array's size. Symbolic constants have a significant advantage over hard-coding a literal value (e.g., "100") in a program: it only takes a single edit to update all the statements using the symbolic constant.
  2. Demonstrates using a variable to specify an array's size. The example reads a value from the console, saves it in the variable, and dynamically allocates the requested memory with the new operator. The same variable used to specify the array size also controls the statements accessing the array, allowing arrays of different sizes each time the code runs.
For correct and secure operation, programs must always be able to determine the size or length of an array, which generally means that programmers must save the size in a separate variable. C++ arrays are a primitive or fundamental data type that does not have a .length attribute. So, the Java notation test.length does not work in C++ programs.

Size vs. Capacity: Partially Filled Arrays

However, once the program creates an array, either automatically or dynamically, its size is fixed: it cannot shrink or expand while the program runs. So, programmers often create arrays with a capacity that exceeds the anticipated need, implying that some of the array elements are unused. In this case, we must be careful when referring to an array's size: are we referring to the array's total capacity or the number of elements used or filled?

int array[8];

for (int i = 0; i < 4; i++)
	array[i] = i;
A picture of a partially filled array. The total capacity is the number of elements in the array. The example uses the first four elements, saving useful data in them. The last four elements are empty - they contain random values but no useful data. Capacity = filled + empty.
(a)(b)
A partially filled array. It's often difficult to know an appropriate array size at compile time. Making it too large wastes some memory when it has unused elements but otherwise causes no harm. Alternatively, a program cannot exceed an array's size, so making the array too small can cause the program to fail.
  1. A code fragment illustrating how an array may become partially filled
  2. An abstract representation of a partially filled array
The best practice is to use the total capacity for the size when storing data in an array and use the number of filled or used elements when accessing data. Following these "rules" prevents indexing an array out of bounds and using an uninitialized array element.