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) |
[
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)
.&
, unnecessary.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]; |
int* scores2 = new int[50]; |
(a) | (b) |
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.
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:
100
#define SIZE 100
enum { SIZE = 100 };
const
keyword: const int SIZE = 100;
Creating arrays dynamically on the heap with the new
operator is more flexible than creating them automatically on the stack. 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 Allocation | Dynamic/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) |
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.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) | (b) |