9. Pointers and arrays

Many parts of the C++ language are inherited directly from the programming language C, from which it was originally derived. C has often been described as a “middle-level programming language”, since its abstractions lie between low-level machine language and assembly language, and high-level languages like Python and Ruby.

C++ retains the “close to the machine” language features from C, though we have been mostly avoiding them so far in this book. It’s time now to look deeper into what is going on inside our computer when we make use of them in C++.

9.1. Variables and values revisted

We were first introduced values and variables back in the Variables, types and expressions chapter. We defined a variable as “a named location that stores a value.” Actually, in the following code:

int n = 3;
n = n + 2;

the two occurences of the variable n in the second assignment mean very different things. On the left hand side of the assignment operator n represents the address in memory where n is located. In the expression on the right hand side of the assignment statement, n represents the value currently stored in the memory location n, which in this example is the integer 3. This second assignment can be read as “take the value stored in the location referenced by the name n, add 2 to it, and then store it back in that same location.”

The two sides of an assignment statement in C++ are given special names, lvalue and rvalue, to reflect this important distinction. Data values can not be lvalues, which is why

1 = n  // WRONG!

will cause a compiler error.

9.2. References revisted

In Pass by reference we introduced the address-of operator, &, though we didn’t call it that yet. As it’s name indicates, this operator yields the memory address of the variable it is applied to.

Let’s look at a program that tests two functions, wont_swap and will_swap, to see how the address-of operator works. Note that both use a type cast, long(), like we did way back in the Converting between types section, to display addresses in decimal notation. We need long() here, since addresses on most machines these days are 64 bits.

#include <iostream>
using namespace std;

void wont_swap(int a, int b)
{
    cout << "a: " << a << "  b: " << b << endl;
    cout << "address of a: " << long(&a) << endl;
    cout << "address of b: " << long(&b) << endl;
    int temp = a;
    a = b;
    b = temp;
    cout << "a: " << a << "  b: " << b << endl;
}

void will_swap(int& a, int& b)
{
    cout << "a: " << a << "  b: " << b << endl;
    cout << "address of a: " << long(&a) << endl;
    cout << "address of b: " << long(&b) << endl;
    int temp = a;
    a = b;
    b = temp;
    cout << "a: " << a << "  b: " << b << endl;
}

int main()
{
    int x = 7;
    int y = 11;

    cout << "x: " << x << "  y: " << y << endl;
    cout << "address of x: " << long(&x) << endl;
    cout << "address of y: " << long(&y) << endl;
    cout << "Calling wont_swap..." << endl;
    wont_swap(x, y);
    cout << "x: " << x << "  y: " << y << endl;
    cout << "Calling will_swap..." << endl;
    will_swap(x, y);
    cout << "x: " << x << "  y: " << y << endl;

    return 0;
}

When we ran this program, we got:

x: x  y: y
address of x: 140730371504847
address of y: 140730371504846
Calling wont_swap...
a: x  b: y
address of a: 140730371504796
address of b: 140730371504792
a: y  b: x
x: x  y: y
Calling will_swap...
a: x  b: y
address of a: 140730371504847
address of b: 140730371504846
a: y  b: x
x: y  y: x

The address values will almost certainly be different on your computer, but the important point here is the relationship between the addresses of x and a, and y and b. In wont_swap they are different, since it is passing by value. In will_swap they are the same.

9.3. Pointers

We turn now to pointers. A pointer is a variable that stores a memory address. Pointers “point to” a storage location that holds an object of another type. So we have pointers to char, pointers to int, pointers to double, etc. The following creates a variable cp that points to a character variable:

char letter = 'a';
char* cp = &letter;

cout << "letter: " << letter << endl;
cout << "address of letter: " << long(&letter) << endl;
cout << "value of pointer to letter: " << long(cp) << endl;
cout << "address of pointer to letter: " << long(&cp) << endl;
cout << "dereferencing the pointer cp gives: " << *cp << endl;

Running this will yield something like this:

letter: a
address of letter: 140725200837471
value of pointer to letter: 140725200837471
address of pointer to letter: 140725200837472
dereferencing the pointer cp gives: a

In the first line we create a variable of type char named letter and assign it the value 'a'. The second line creates a pointer to char variable named cp and initializes it to the address of letter. Using the first three cout statements, we confirm that letter does indeed store the character 'a', and that cp stores the address of letter.

The next cout statement shows where cp itself is stored, and the last cout statement introduces the dereference operator, *, which returns the value at the memory location to which the pointer points.

The use of the * symbol in two different ways here is confusing. When applied to an lvalue, as in:

int* ip;

it means “pointer to” the type to which it is applied. When it appears in front of a pointer variable as part of an rvalue, it is the dereference operator, returning the value referenced by the pointer.

Here is another working version of our swap function that uses pointers:

void will_swap_with_pointers(char* a, char* b)
{
    cout << "a: " << *a << "  b: " << *b << endl;
    cout << "address of a: " << long(a) << endl;
    cout << "address of b: " << long(b) << endl;
    int temp = *a;
    *a = *b;
    *b = temp;
    cout << "a: " << *a << "  b: " << *b << endl;
}

and a few more lines to call this new version in main:

char c = 'c';
char d = 'd';
cout << "Calling will_swap_with_pointers..." << endl;
will_swap_with_pointers(&c, &d);

Notice that since the type of the two parameters in this function is pointer to char, we need to send it the address of c and d using the address-of operator.

9.4. Dynamic memory allocation and memory leaks

Using the address-of operator is not the only way to assign a value to a pointer. The new operator takes a type and returns an address of memory large enough to store that type.

int* ip = new int(42);
cout << "ip: " << long(ip) << endl;
cout << "dereferencing the pointer ip gives: " << *ip << endl;

Running this will yield something like:

ip: 94572116348592
dereferencing the pointer ip gives: 42

This dynamic memory allocation can be useful, but it requires us to take responsibility for handling memory management ourselves, and it comes with a danger, we can leak memory! A memory leak occurs when we dynamically assign memory to a pointer and then reassign that pointer to point somewhere else.

ip = new int(5);
cout << "ip is now pointing to: " << long(ip) << endl;
cout << "dereferencing the pointer ip gives: " << *ip << endl;
cout << "But we leaked memory!" << endl;

which yields something like:

ip is now pointing to: 94572116349664
dereferencing the pointer ip gives: 5
But we leaked memory!

The problem is that the memory located at address 94572116348592 is still listed as used by the operating system, but it is no longer available to our program, since we have no way to address it.

When allocating our own memory using new, we need to explicitly free it up with delete

int* ip = new int(42);
int* tip = ip;
ip = new int(5);
delete tip;

The new operator gives us dynamically allocated memory, called heap memory. We need to manage this memory ourselves, allocating it and deallocating it. Until now we have been using stack memory, which is automatically allocated for us.

Each type of memory allocation has its advantages and disadvantages. Further exploration of this advanced topic is left as an exercise.

9.5. Arrays

An array is a fixed sized sequential collection of elements of the same type. Each element in the array is addressed by its index, beginning with 0.

We declare an array by putting its size in square brackets next to the name of the array. Each element is then accessed using the name of the array followed by its index in square brackets.

int nums[4];

creates an array of four integers named nums. The individual integers are called elements of the array.

The [] operator reads and writes elements of an array the same way it accesses the characters in a string. As with strings, the indices start at zero, so nums[0] refers to the “zeroth” element of the array, and nums[1] refers to the “oneth” element. You can use the [] operator anywhere in an expression.

The elements of an array can be intialized by enclosing their intial values in {} curly braces:

int nums[4] = {0, 2, 32, 42};

creates and array of four integers and initializes them to 0, 2, 32, and 42.

The following code creates an array of four integers, initializing them all to 0, and then changes each element, assigning it a new value:

#include <iostream>
using namespace std;

int main()
{
    int nums[4] = {0, 0, 0, 0};
    nums[0] = 7;
    nums[1] = nums[0] * 2;
    nums[2]++;
    nums[3] -= 60;
    cout << nums[0] << ' ' << nums[1] << ' ' << nums[2] << ' ' << nums[3];
    cout << endl;
    return 0;
}

All of these are legal assignment statements. Here is the effect of this code fragment:

nums state diagram

Since elements of this array are numbered from 0 to 3, there is no element with the index 4.

You can use any expression as an index, as long as it has type int. One of the most common ways to index an array is with a loop variable. For example:

int i = 0;
while (i < 4) {
    nums << nums[i] << endl;
    i++;
}

This while loop counts from 0 to 4. When the loop variable i is 4, the condition fails and the loop terminates. Thus, the body of the loop is only executed when i is 0, 1, 2, and 3.

Each time through the loop we use i as an index into the vector, outputting the ith element. This type of array traversal is very common. Arrays and loops go together like fava beans and a nice Chianti.

The following program uses a for loop to set the values of an array of integers to the square numbers from 0 squared to 9 squared.

#include <iostream>
using namespace std;

int main()
{
    int square_numbers[10];
    for (int i = 0; i < 10; i++)
        square_numbers[i] = i * i;
    return 0;
}

Its state diagram looks like this:

square_numbers array

Running:

cout << "square_numbers[7] is " << square_numbers[7] << "." << endl;

will produce:

square_numbers[7] is 49.

What happens if we run the following?

cout << "square_numbers[12] is " << square_numbers[12] << "." << endl;
cout << "square_numbers[13] is " << square_numbers[13] << "." << endl;
cout << "square_numbers[14] is " << square_numbers[14] << "." << endl;

We avoided discussing what happens when we run something like this when talking about strings. It will give us a surprise!

One run of this on our machine gave the following:

square_numbers[12] is 1.
square_numbers[13] is 0.
square_numbers[14] is -2078020166.

Arrays in C++ are memory unsafe, meaning you can use them to access places in memory – like that addressed by square_numbers[12] – which are not within the boundaries of your array.

C++ will compile this statement without objection. When you run it, you will get an arbitrary value of whatever happens to be stored in that memory location. The technical term computer scientists use for arbitrary data like this is garbage, and it can cause havoc in your programs. The expression “garbage in, garbage out” captures the problem. Any computation that uses a garbage value will itself become garbage, making programs very hard to debug.

9.6. Pointers and arrays

Arrays and pointers in C++ are closely related. To quote Brian Kernighan and Dennis Ritchie from The C Programming Language, “In C, there is a strong relationship between pointers and arrays, strong enough that pointers and arrays should be discussed simultaneously.”

To see this relationship in action, add the following lines after the square_numbers initialization loop in the previous section:

int* iptr = square_numbers;
cout << "The 8th of our square numbers is " << *(iptr + 7) << "." << endl;

The array variable square_numbers, without using the square brackets, is a pointer to the address of the first integer in the array, as we can see here, where a pointer variable iptr is assigned to it. Incrementing the pointer varialble makes it point to the next element in the array. Incrementing it by 7 points it to the 8th element of the array.

9.7. Dynamic memory allocation with arrays

There are two dynamic memory operators, new[] and delete[] for allocating memory for arrays. The following program uses these two operators to create our array of square numbers in the heap, and then prints them out two different ways.

#include <iostream>
using namespace std;

int main()
{
    int* square_numbers = new int[10];
    // set the elements to their square number values
    for (int i = 0; i < 10; i++)
        square_numbers[i] = i * i;
    // print them out 
    for (int i = 0; i < 10; i++)
        cout << square_numbers[i] << ' ';
    cout << endl;
    // print them out another way
    int* ip = square_numbers;
    for (int i = 0; i < 10; i++)
        cout << *ip++ << ' ';
    cout << endl;
    // all done, DON'T FORGET TO FREE THE MEMORY!
    delete[] square_numbers;
    return 0;
}

9.8. C Strings

At the beginning of the Strings chapter, we mentioned native C strings, stating that we lacked concepts needed to introduce them. Now we have them.

A string in the C programming language is an array of characters (char) ending in a special character called the null character, which is a char with a numeric value of zero ('\0').

#include <iostream>
using namespace std;

int main()
{
    // Create a C string
    char s[] = "C string";
    // print the C at index 2
    cout << s[2] << endl;
    // print the entire string
    int i = 0;
    while (s[i])
        cout << s[i++];
    cout << endl;
    // print again using pointers 
    for (char* cp = s; *cp != '\0'; cout << *cp++);
    cout << endl;
    return 0;
}

When the compiler sees a string literal enclosed in quotation marks ("") intializing a character array, it appends a null character to the end of the array of charactes. This convention is utilized in each of the two loops that print the string.

An alternative, and much more tedious, way to create the same C string is the following:

char s[9] = "C string";
s[0] = 'C';
s[1] = ' ';
s[2] = 's';
s[3] = 't';
s[4] = 'r';
s[5] = 'i';
s[6] = 'n';
s[7] = 'g';
s[8] = '\0';

It is crucial to remember to both allocate space for and set the last character in the array to null.

9.9. Null pointers

C++ defines a special value called a null pointer for a pointer variable that does not point to a valid memory address. Since C++11 this value has been represented by the keyword nullptr. Earlier versions of C++ used 0 or the defined value NULL.

9.10. Command-line arguments

The main function in C++ can accept command-line arguments through two parameters, argc and argv.

The following program shows how they work:

#include <iostream>
using namespace std;

int main(int argc, char* argv[]) {
    for (int i = 0; i < argc; i++) {
        cout << "Command line argument [" << i << "] is: ";
        cout << argv[i] << endl;
    }
    return 0;
}

If this program is compiled with:

$ g++ command_line_arguments.cpp -o clargs

and then run with:

$ ./clargs this that 42 such and such

it will produce the following output:

Command line argument [0] is: ./clargs
Command line argument [1] is: this
Command line argument [2] is: that
Command line argument [3] is: 42
Command line argument [4] is: such
Command line argument [5] is: and
Command line argument [6] is: such

Here’s another version of the program to emphasize once again the intimate relationship between arrays and pointers:

#include <iostream>
using namespace std;

int main(int argc, char* argv[]) {
    int i = 0;
    for (char** clp = argv; *clp != nullptr; clp++) {
        cout << "Command line argument [" << i++ << "] is: ";
        cout << static_cast<char*>(*clp) << endl;
    }
    return 0;
}

The parameter argv is an array of C strings, which as we now know, are themselves arrays of characters terminated by a '\0' character.

Given our understanding of the relationship between arrays and pointers – an array variable is an address in memory where a contiguous sequence of elements begins – we can see that argv[] is an array of pointers.

What’s more argv itself is a pointer to a pointer. In fact, in the same way that C strings are terminated by a null character, the sequence of pointers in argv is terminated by the null pointer, nullptr.

So in the for loop we create a loop variable clp of type char**, a pointer to a pointer to a char, and initialize it to argv. We continue looping until the pointer to which clp points is nullptr, incrementing it each time to make it point to the next pointer.

If looking at the second version of this program is making you dizzy, don’t despair, but do keep trying to understand how it works. When you do, you will have come a long way towards understanding pointers.

9.11. Glossary

lvalue

A reference to a storage location in memory. When a variable appears on the left hand side of an assignment statement, it refers to the memory location where the rvalue will be stored.

pointer variable

A variable that holds an address in memory. Pointers can be initialized after they are created, and can be reassigned to different address values.

reference variable

A variable that holds the address of another variable. References can not be reassigned once they are created.

rvalue

A value appearing on the right hand side of an assignment statement. The rvalue is stored in the location reference by the lvalue.

9.12. Exercises