Review of last time
References: references are just another name for an existing box:
int a = 1;
int& b = a; // Now b is another name for a's box
Pointers: pointers are like references that can change what they refer to while the program is running:
int a = 1, b = 2;
int* c = &a; // c points to a
*c = 3; // Now a = 3
c = &b; // c points to b
*c = 12; // Now b = 12
References are interesting when we use them as the arguments to functions.
We can create references and pointers to anything that has its own box. This includes elements of arrays, vectors and strings.
We can create pointers to pointers:
int a = 1;
int* b = &a; // b points to a
int** c = &b; // c points to b
An example: Let’s write a function which takes two pointers and returns a pointer to the smallest pointed-to value:
int* min(int *a, int *b) {
if(*a < *b) {
return a;
}
else
return b;
}
// Using the conditional operator:
int* min(int* a, int *b) {
return *a < *b ? a : b;
}
Another example: Let’s write a function which searches a vector for a given
value. If it exists, we return a pointer to it. If it does not, we return
nullptr
:
int* find(vector<int> data, int target) {
for(int i = 0; i < data.size(); i++)
if(data.at(i) == target)
return &(data.at(i));
return nullptr;
}
Can we use a ranged-for loop for this? Yes, provided we remember to use a reference:
int* find(vector<int> data, int target) {
for(int& e : data)
if(e == target)
return &e;
return nullptr;
}
What happens if we leave off the reference on the loop variable? Then it’s the same as if we had written
int* find(vector<int> data, int target) {
for(int i = 0; i < data.size(); i++) {
int e = data.at(i);
if(e == target)
return &e;
}
return nullptr;
}
Here we’re returning a pointer to e
, but e
has no connection with data
,
it’s an independent variable. The address of e
is basically worthless, because
it goes away after the loop.
If we create e
as a reference to the elements of the vector, then getting
the address of e
is the same as getting the address of data.at(i)
for some
i
.
Pointers to functions
In addition to pointers to values (and pointers-to-pointers, etc.) we can create pointers to functions. This allows us to call a function, without knowing exactly which function will be called!
Here’s an example:
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
cout << "Enter two numbers: ";
int a,b;
cin >> a >> b;
cout << "Do you want to add or subtract (a/s)? ";
char choice;
cin >> choice;
int (*operation)(int,int) = (choice == 'a' ? &add : &subtract);
cout << "The result is " << operation(a,b) << endl;
return 0;
}
The line int (*operation)(int,int)
creates a pointer-to-function named
operation
. The functions that it may point to must return an int
, and must
take two int
s as parameters. We use a conditional operator to initialize it
to either add
or sub
. Note that for functions, &add
is equivalent to
add
by itself, so we could simplify this line to
int (*operation)(int,int) = (choice == 'a' ? add : sub);
To use a function pointer, just pretend that it’s a function and call it normally:
cout << "The result is " << operation(a,b) << endl;
Depending on the user’s input, operation
will either point to add
or sub
,
which determines which operation will be performed.
Function pointers can be combined with arrays and vectors, to create collections of related operations, provided they all have the same signatures (return type and argument types):
vector<int(*)(int,int)> functions;
functions.push_back(add);
functions.push_back(subtract);
(functions.at(0))(1,2); // returns 3
(functions.at(1))(1,2); // returns -1
(The type int(*)(int,int)
is the type of “pointers to functions that
return an int and take two int arguments”.)
Similarly, we can write a function which takes a function pointer:
int square(int x) {
return x * x;
}
int run_twice(int x, int(*f)(int) ) {
return f(f(x));
}
...
run_twice(2, square) // evaluates to (2*2) * (2*2) = 16
This is yet another way of abstraction out a facet of a functions operation: you can let the user “plug in” some details by providing function pointers.
An Example
As an example of how function pointers can be useful, let’s write a function that can be used to perform the same effect as almost any accumulating loop. Remember that for an accumulating loop we need two things:
The operation to perform inside the loop (
a = a OP e;
)The identity value of the operation
OP
Assuming we have a vector of int
s, and that the type of x
is also int
,
then the type of OP
is int(*)(int,int)
; i.e., it’s a function pointer that
takes two ints, and returns an int. Our function now looks like this
int accumulate(vector<int> data,
int identity,
int(*operation)(int,int)) {
int a = identity;
for(int e : data)
a = operation(a,e);
return e;
}
If we do
int x = accumulate(data, 0, add);
I’ll get the sum. If we do
int multiply(int x, int y) { return x*y; }
accumulate(data, 1, multiply);
I’ll get the product.
We can even make a version that finds the smallest (or largest). In this case,
the operation is min
, a function that returns the smaller of its two
inputs:
// Note: this function is already defined in #include<utility>
int min(int a, int b) {
return a < b ? a : b;
}
accumulate(data, data.at(0), min) // returns the smallest element
Another example:
Dynamic memory
One of the major restrictions on arrays is that they must have a size that is fixed when your program is created. E.g., you cannot do something like this:
int n;
cin >> n;
int arr[n]; // Create an array of size n; THIS DOES NOT WORK
This raises the question of how things like string
and vector
can
possibly work? They aren’t magic; string
and vector
were both written,
in C++, by somebody, so there must be some way to store a variable amount of
“stuff”.
Put another way, without string
and vector
, there is an exactly one-to-one
correspondence between the number of variables in our program, and how
values we can store. If you want to store 10 char
values (without string
),
then you’d need 10 char
variables, or a char
array of size 10. If you
don’t know how many char
s you need (as is the case with a string
which can
shrink and grow while a program is running), how can we do this?
While the number of variables in our program is fixed when we write it (you
could go through and count exactly how many char
variables you have), we can
request “unnamed” values, at runtime. The values have to be unnamed (not
linked to variables) because we don’t know how many we’ll need until the
program is running. But if a value is unnamed, how can we manipulate it? We
have to use the only tool we have for talking about things without using their
names directly: pointers.
Here we’re going to see how we can create space for both single objects and arrays at runtime, with a dynamic size like n, above.
To create an object at runtime, we use new
:
int* i = new int(); // returns an int*
char* c = new char(); // returns a char*
string* s = new string(); // returns a string*
new
creates a new value of the type requested, and returns a pointer to it.
It returns a pointer, because the new value is not attached to any particular
name; no variable “owns” it.
You might wonder what the difference is between doing
string s;
and doing
string* s = new string();
and the difference has to do with lifetime.
Scope and lifetime
Scope is the region of your program where a given variable is accessible. It is part of your program from the start; you don’t need to run your program in order to find out the scope of something, you can just look at it. We call a property like this, that you can determine just by looking at the source code, a static property.
There is an analogous property for values that is called lifetime. The lifetime of a value is how long, at run time, will that value continue to exist. (Properties like this, that can be determined in the last resort, only by running the program, are called dynamic properties.)
Normally, the lifetime of a value is tied to the scope of the variable that contains it. When we write:
void f() {
string s;
...
}
The string inside s has a lifetime that begins when f
starts, and ends
when f
returns. That is, it’s basically the same as the scope of s.
When I create a value with new
, it has potentially unlimited lifetime:
void f() {
string* s = new string();
...
}
The string pointed to by s does not go away when f
returns. It lives on.
It is no longer accessible (because s, the only way to access it, went out
of scope), but it still exists, and it still takes up memory. If we had
preserved the pointer to it in s, we could continue to use it after f
returned.
Another way to think of this is that new
creates an unnamed box. Because it
has no name, we can conclude two things about it:
The only way to access it is through a pointer.
It’s lifetime is not bound to the scope of any particular name.
How long is the lifetime of objects created with new
? Well, in the last resort,
all objects die when your program quits. So theoretically, until the end of your
program. But this is usually not what we want, so instead, we can say when
an object’s lifetime should end ourselves:
delete s;
If new
creates a free-floating object, delete
destroys one. After delete s;
,
the pointer s
is no longer valid. In fact, it’s good practice to set a pointer
to null when you delete
it:
delete s;
s = nullptr;
The stack and the heap
To understand why things created via new
have a different lifetime, we need to
talk about the two parts of memory that our programs can use: the stack and
the heap.
When we write a function (including main
) that declares some variables, where
in memory do those variables exist? They exist on the stack. The stack works
like this:
Every function, when it starts up, has an activation record created for it on top of the stack. This contains things like space for its parameters and variables.
When a function returns, it’s activation is removed from the top of the stack. The record below it is that of the function that called it, which picks up where it left off. Removing a record from the top of the stack also signifies the end of the lifetimes of all its variables, because they were stored in that record.
Thus, the stack forms a literal stack: you put things on top and they cover up the things below them, but when you remove the top thing, you get access to whatever was originally below it. This is how functions keep track of who called who, and what the different functions were originally doing.
E.g., suppose we have these functions:
int main() {
int x;
f();
g();
h();
}
void f() {
int y;
h();
g();
}
void g() {
int z;
h();
}
void h() {
int x;
}
(Run through this).
When a function returns a value (not a reference) it must make a copy:
void f() {
int y = g();
}
int g() {
int x = 12;
return x;
}
The value in x is copied into the record for f
as part of the return process.
This is why we can return a reference to a variable within g
, because the
record containing that variable will have disappeared after the return
is complete.
The stack is nice because it cleans up after us, we don’t have to worry about manually deleting variables when we’re done with them. But if we want more control over the lifetime of the values, we can’t use the stack, because it handles lifetimes automatically. If I want to create an object in one function, return it (not a copy of it), and then release it in a different function, the stack won’t work for that. So there’s another region of memory, for manually-managed objects: the heap.
The heap is just a big chunk of memory dedicated to your program. When you do
new string
new
goes out and finds some unused portion of it big enough to
store a string
, and reserves it, and then gives you back a pointer to that
part of memory. Because it’s not on the stack, that memory is not affected by
the normal function call-return process; it lives until you tell it to go away.
Which you do by delete
-ing it. delete
-ing a piece of the heap just tells the
system to make it as unused again, so that it’s free to be used to satisfy
another new
later on. (Note that you cannot delete
stack-based variables,
don’t even try, or terrible things will happen.)
This also explains why new
and delete
only work with pointers. Only variables
that are on the stack have names. The heap has no names, just chunks of memory
that have been reserved for various purposes. The only way to access such
a piece of memory is thus through a pointer.
There isn’t much point (right now) in creating single values stored on the heap, usually we use it for arrays, but let’s do an example, to illustrate both how to use this memory, and how to use pointers to manipulate objects. This program prompts the user to enter their name (first and last), and then prints it out as first-initial last name. E.g., if you enter “Susan Smith” it will print “S. Smith”.
int main() {
string* name = get_name();
cout << first_initial(name) << ". "
<< last_name(name) << endl;
delete name;
return 0;
}
char first_initial(string* name) {
return (*name).at(0);
// can be shortened to name->at(0);
}
string last_name(string *name) {
int begin = (*name).find(" ") + 1;
return (*name).substr(begin, (*name).length() - begin);
}
Why does last_name
return a string
and not a string*
? Partly because that’s
what substr
returns. Let’s see what we have to do in order to make it return
a string*
:
string* last_name(string *name) {
int begin = (*name).find(" ") + 1;
// Create a string on the heap
string* last = new string;
// *Copy* the substring containing the last name into it
*last = substr(begin, (*name).length() - begin);
return last;
}
(Writing (*name).length()
and such is so cumbersome that there’s a shorthand
for doing this: name->length()
. Whenever I might write (*thing).whatever
I can write thing->whatever
instead.)
But now we have to go and fix main
: cout
doesn’t know how to print a
string*
, it only knows how to work with string
. So we have to do
int main() {
string* name = get_name();
string* last = last_name(name);
cout << first_initial(name) << ". " << *last << endl;
delete name;
return 0;
}
What’s wrong with this code? I forgot to delete last
! This illustrates some
of the pitfalls of working with new
memory and pointers: most of the tools
we have aren’t setup for working with them, they’re more of a low-level detail
that is normally hidden. You also have to be very careful to delete
things
that you create. Normally, that responsibility is hidden from us (e.g.,
string
and vector
use new
and delete
internally, but they handle
calling delete
when necessary); if we’re going to use heap-allocated memory,
then suddenly we have the responsibility of cleaning up after ourselves.
Dynamically creating arrays
To create an array using new
, we write this:
int* data = new int[10](); // Create an int array of size 10.
Once created, you can access the elements of it the same way as for a normal array:
data[0] = 1;
The interesting bit is that we do not have to use a constant like 10, we can just as well use a runtime value:
int main() {
cout << "How many values are you going to enter? ";
int size;
cin >> size;
int* data = new int[size]();
// Read in data
for(int i = 0; i < size; i++)
cin >> data[i];
// do stuff
// Release the data
delete[] data;
return 0;
}
The line before the final return
illustrates that there is a different
syntax for deleting an array, vs. a single value. To delete an array you
must use
delete[] arr;
The square brackets after delete
tell the system that it is an array you are
deleting. If you forget them, your program may crash in bizarre ways (e.g.,
it might crash later, after the delete, in some unrelated part of your program!).
Note that the two other restrictions on array still apply to these:
You cannot grow or shrink an existing array
The size is not stored anywhere, you have to keep track of it yourself, or risk going past the end.
The size restriction is particularlly irksome, because it means that if we want to write a function that operates on a dynamic array, we have to include the size as another parameter:
int sum(int* data) {
for(int i = 0; i < ???) // no way to find out the size!
}
int sum(int* data, int size) {
int s = 0;
for(int i = 0; i < size; i++)
s += data[i];
}
Quiz: how do I write a pointer-based loop for the above?
int sum(int *data, int size) {
int s = 0;
for(int* p = &data[0]; p != &data[size]; p++)
s += *p;
}
Note that with a dynamically allocated array, you cannot use a range-based
for
loop. A normal, statically created array has its size “hidden” somewhere
with it, so that the range-based for
knows when to stop. A pointer is just a
pointer; it has no size information (this is also why we have to remember to
use delete[]
instead of delete
, because the system doesn’t know whether a
pointer is to an array, or to a single object).
Array to pointer conversion
A statically-allocated array is a different kind of thing from a pointer to a
dynamically-allocated array, but we can convert the former into the latter.
(Note that this loses the hidden size information!) Basically, a static array
of (say) int
can be implicitly converted to an int*
:
int data[] = {1, 2, 3, 4, 5};
int x = sum(data, 5);
The array data, when converted to an int*
, converts to a pointer to
data[0]
. In otherwords, the above is exactly equivalent to
int data[] = {1, 2, 3, 4, 5};
int x = sum(&data[0], 5);
This means that any functions we write that work with arrays created via new
can also be used with static arrays, transparently.
This also gives us a new way to write arr[x]
:
int arr[] = {1, 2, 3, 4, 5, 6};
int* p = arr; // points to arr[0]
*(p + 2) // accesses p[2]
p[2] // also accesses p[2]
Advancing a pointer by x and then accessing it is exactly the same as writing
p[x]
; the second is just a shorthand for the first.
Examples
Using arrays created via new
and delete
is basically like using standard
arrays, you just have to remember to free them after you’re done.
Reading in some numbers from the user:
int* read_numbers() {
cout << "How many numbers are you going to enter? ";
int n;
cin >> n;
int* data = new int[n]();
cout << "Enter numbers: ";
int i = 0;
while(i < n) {
cin >> data[i];
i++;
}
return data;
}
Note that this illustrates an important point: a function can create an array
via new
and then return it, and it’s the responsibility of the calling
function to free it when it’s done. E.g., our main
might look something like
int main() {
int* nums = read_numbers();
// Do something with nums
delete[] nums;
return 0;
}
Checking whether an array contains a number:
int* find(int* data, int size, int value) {
for(int *p = data; p != data + size; p++) {
if(*p == value)
return p;
}
return nullptr;
}
This function returns either a pointer to the element containing value
in the
array, or nullptr
if it is not found. If we want to know the index within
the array where the item is found, we can just do
find(data, 100, 12) - data
Remember that pointer subtraction gives the number of elements between them.
Do we need to delete
the pointer returned by find
? No! In fact, terrible
things will happen if we do. The pointer returned by find
is a pointer to
inside an array; we only ever want to delete whole arrays. (And note that
find
will work on static arrays, which we don’t ever want to delete at all.)
One nice this about using pointers as arrays is that it lets us lie about how big an array is. E.g., suppose we had the array
int arr[] = {1, 5, 2, 7, 5, 2, 8, 5, 2};
And we wanted to find the sum of elements 2 through 5. We can use sum
to do
this: we just give it a pointer to arr[2]
as the “start” of the array, and
tell it that the size is 4:
int x = sum(arr + 2, 4); // We could also write &arr[2]
sum
doesn’t know that the array is actually bigger, that there’s more of it
that it can’t see. Sometimes, we can use this to simplify our code.
Let’s step up our game a bit. Suppose I give you a sorted array, in ascending order:
int arr[] = {1, 2, 5, 6, 8, 9, 11, 15, 17, 20, 21};
and we want to find a particular element in this array, say, 11. Can we do better than just scanning through the array from beginning to end? As a hint, look at middle element, 9. What can you say about where 11 will be, based on the fact that the middle element is 9?
11 must be after 9 in the array. Because the array is sorted, we know that
everything before 9 will be less than or equal to 9, so we can exclude that
entire region of the array from our search. We can exploit this to write a
find
that will work faster on sorted arrays:
int* find_sorted(int value, int* begin, int* end) {
int* b = begin;
int* e = end;
while(begin <= end) {
int* middle = begin + (end - begin) / 2; // Pointer to the middle element
if(*middle == value)
return middle;
else if(*middle < value) {
end = middle - 1;
}
else if(*middle > value) {
begin = middle + 1;
}
}
return nullptr; // Not found
}
This is called a binary search. At each step, we cut the amount of things
we have to search through in half. Contrast this with the earlier find
, where
at each step, we only reduced the amount of things we had to search through
by 1.
Also note that we can use both find
and sorted_find
on “sub-arrays”. If
the pointers we give them are not for the whole of an array, but just for a
part, they don’t care.
new
and delete
are dangerous
It’s true, they are. The reason they are dangerous is that it’s very easy to
forget to delete
something, or let the only pointer to it go out of scope,
etc. However it happens, you now have a memory leak. If you write a function
like this:
void my_function() {
int* stuff = new int[10]();
//.. does some stuff
// does NOT delete stuff
}
Every time you run this function, you leak 10 int
s worth of memory. Your
program will never get that memory back; the only way to get it back is to
close your program. Contrast this with something like
void my_function() {
vector<int> stuff(10);
//...
// stuff is automatically freed here
}
You should prefer structures like vector
and string
that manage their own
memory, rather than trying to roll your own. The only time you should be using
new
and delete
are when you need pointers (sometimes) or when you are
actually creating your own data type. If you are building vector
yourself,
you can use new
and delete
to do so.
Old-style strings
In C (and in very old C++), before string
was invented, strings were represented
as dynamically allocated char
arrays (i.e., as char*
). Because people were
lazy and didn’t want to pass the length of the string around, they came up with
the idea of marking the end of the string with a special character: the null
character, \0
. That is, every string would actually look something like this:
"Hello, world!\0"
If we wanted to know the length of the string, we just scan down it until we
find the \0
, counting characters as we go.
The header <cstring>
contains a number of functions for manipulating these
kinds of “strings”, in particular, strlen
which finds the length of a string.
Let’s use this to write a function to append one char*
string onto the
end of another. That is, we’re going to write s1.append(s2)
for char*
style strings.
Before we can create a dynamically-allocated array, we have to know how
big to make it. So we need to know the length of the string that will result
from appending s1
to s2
:
char* append(char* s1, char* s2) {
int l1 = strlen(s1), l2 = strlen(s2);
char* out = new char[l1 + l2];
// Copy all the elements of s1
for(int i = 0; i < l1; i++)
out[i] = s1[i];
// Copy all the elements of s2
for(int j = 0; k < l2; j++)
out[j + l1] = s2[j];
return out;
}
Old-style strings were very dangerous; all sorts of bugs and exploits resulted
from accidentally writing strings into arrays that weren’t allocated big
enough to hold them. Nonetheless, there’s still a lot of old-style code
floating around (esp. in C, but even in C++). Note that string
will even
give you access to the char*
representing its text:
string name = "Andy Clifton";
name.c_str() // Gives the null-terminated char* representing "Andy Clifton\0";