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 string
s, 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:
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:
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.