Chapter 8
IN THIS CHAPTER
Using two types of memory: the stack and the heap
Accessing variable addresses through pointers
Creating variables on the heap by using the new keyword
Taking pointers as parameters and returning pointers
Where do you live? Don’t say it out loud, because thousands of people are reading this book and you don’t want them all to know. So just think about your address. Most places have some sort of address so that the mail service knows where to deliver your packages and the cable guy can show up sometime between now and 5:00 next Thursday. (So make sure that you’re there.)
Other things have addresses, too. For example, a big corporation in an office building likely has all its cubes numbered. Offices in buildings usually have numbers, and apartments normally have numbers, too.
Now suppose that someone named Sam works in office number 180. Last week, however, Sam got booted out the door for spending too much time surfing the web. Now Sally gets first dibs on office number 180, even though she’s not taking over Sam’s position. Sam moved out; Sally moved in. Same office — different person staying there.
The computer’s memory works similarly. Every little part of the computer’s memory is associated with a number that represents its location, or address. In this chapter, you discover that after you determine the address of a variable stored in memory, you can do powerful things with it, which gives you the tools to create powerful applications.
If you don’t understand pointers at all, you might want to first read the rest of the chapter, starting with “Heaping and Stacking the Variables,” and return to this first section later. Readers who already know something about pointers need to be aware of the changes in pointers for C++ 20, which is why it appears first. The essential thing to remember as you move to C++ 20 (where new
is deprecated) and then to C++ 23 (where new
is removed) is that pointers are going to change.
C++ will always need pointers, of course, but long-time C++ users have always seen pointers as a burden, while new C++ users see pointers as some sort of heroic nightmare rite of passage. The goal, then, is to make pointers easier and more consistent to use as C++ continues to grow and mature. The following sections discuss how C++ pointers are changing in C++ 20.
A raw pointer, one that you allocate using the new
operator, serves important purposes in your code. You often see it used for these purposes:
As your knowledge of C++ increases, you soon discover that these are critical application needs, so replacing the raw pointer will be quite difficult. Fortunately, you don’t have to use the new C++ 20 features immediately, even if you’re using a C++ compiler. You control whether your application uses the new approach through compilation directives:
#feature <no_pointers> //opt-in to no pointers
#feature <cpp20> //opt-in to all C++20 features
Consequently, you don’t have to worry about your existing code suddenly breaking. The idea is to make the transition from raw pointers to something better as smooth and transparent as possible. Given the realities of C++ development, you likely will see some sort of legacy support for a long time. However, to move forward, you must adapt to the new realities of pointers in C++.
At this point, you might wonder why raw pointers are such a problem. After all, a pointer is simply an address in memory that looks something like 0x9caef0
. The value it contains is the address, and by dereferencing the pointer, looking at the address to which it points, you see the value that the pointer references. It’s just like the address for your house. You send mail to the address, but the address isn’t your house — it’s simply a pointer to your house.
At this point, it doesn’t sound as if using pointers would be a problem, despite being a bit convoluted. The reason for using pointers in the first place is to avoid carrying large objects around in your code. You can leave the object, like a house, sitting in one place and simply point to it as needed. Imagine having to carry your house around with you. Besides having a horrible backache, doing so would be inconvenient and make your house harder to find. Instead, you give someone who wishes to mail you a letter or visit you in your home the address. Early applications had to use every tiny bit of memory and CPU processing cycles efficiently or face performance issues. Pointers allowed early applications to perform well by simply pointing at big objects in memory, rather than passing them around.
The biggest problem with pointers is the same problem incurred by house addresses. You need to think about the number of times you’ve received your neighbor’s mail (and vice versa). Likewise, applications can have invalid pointers, and when the code tries to process this invalid address, it often crashes the application. Of course, the worst problem is the null pointer, 0x000000
, which you expect to point to something. A null pointer points to nothing.
Another problem with pointers is that you spend a lot of time managing them, and who can remember all that code! Every time you work with pointers, you risk:
To write cleaner code with fewer bugs, you need to find a way to get the effects of a pointer without any of the disadvantages of pointers. The C++ committee has been working on this issue. For example, std::auto_ptr
is deprecated (set for deletion, but still allowed) in C++ 11 and removed in C++ 17. Here are some modern ways of getting past pointers:
std::unique_ptr
or std::shared_ptr
, eliminates the need for you to manage memory manually. Instead, the smart pointer addresses memory management needs for you so that you can concentrate on writing business logic rather than performing low-level programming tasks.std::optional
as the means for working with nullable references. When an optional pointer is null, it has a value of std::nullopt
, which is actually an important thing to know when dealing with them. The only problem is that the implementation is flawed because it lacked support for references (pointers to pointers) and had no monadic (entity operator) interface (see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0798r0.html
for a discussion of this extremely advanced concept not covered in this book). The short version is that it didn’t do what raw pointers could do, but these problems are fixed in C++ 20.obj
, which is outside the function, to directly construct the object being initialized inside the function and that is returned from it. You can view this optimization as: T obj = f();
, where f()
is a function that initializes obj
of type T
. Here is how the optimizations differ:
Return Value Optimization (RVO): In this case, you could have a function that looks like this:
T f() {
… // Do something here.
return T(constructor arguments);
}
In this case, you could create three objects of type T
: the unnamed temporary object created by the return
statement; the temporary object returned by f()
to the caller; and the named object, obj
, copied from the return from f()
. Using RVO eliminates the two temporary objects by initializing obj
directly with the arguments passed inside the body of f()
. This is actually a complex topic that’s well outside the purview of this book, but you can read a discussion of the details of this topic at https://shaharmike.com/cpp/rvo/
.
Named Return Value Optimization (NRVO): This form of optimization goes a step further than RVO when the
return
statement uses a named value, as shown here:
T f() {
… // Do something here.
T result(constructor arguments);
return result;
}
This technique effectively replaces the hidden object and the named object inside the function with the object used for holding the result. The only caveat is that result
must be unique so that the compiler knows which object inside f()
to use to construct the memory in obj
. NRVO is a particular kind of copy elision (the process of joining together or merging of objects) discussed in detail at https://en.cppreference.com/w/cpp/language/copy_elision
.
C++ applications use two kinds of memory:
Suppose that you have a function called GoFishing()
. The function GoFishing()
calls StopAndBuyBait()
. Depending on the complexity of the bait business, StopAndBuyBait()
may call PayForBait()
, which calls GetOutCash()
. How can the computer keep track of all this mess? It uses the stack. Begin with the following code:
int GoFishing() {
int baitMoney = 2;
int numberWorms = StopAndBuyBait(baitMoney);
if (numberWorms > 0) {
return true;
}
return false;
}
int StopAndBuyBait(int customerMoney) {
if (customerMoney > 0) {
int wormsBought = customerMoney * 20;
return wormsBought;
}
return 0;
}
The customer starts out with $2.00. When stopping in the store, the clerk asks for the money. If the customer does have money, the clerk provides 20 worms for each $1.00. The customer determines whether there was enough money to buy any worms. If so, it’s time to go fishing. The stack for each of these calls appears in a stack frame, which the application treats as a single entity for that function. This code uses two stack frames, one for each function call, as shown in Figure 8-1.
FIGURE 8-1: The two stack frames used for the example code.
When GoFishing()
calls StopAndBuyBait()
, it passes a single argument that GoFishing()
sees as baitMoney
. However, StopAndBuyBait()
sees the parameter as customerMoney
. The arguments that GoFishing()
passes to StopAndBuyBait()
appear first as parameters within the stack frame that the application creates for StopAndBuyBait()
, followed by the return address for GoFishing()
. Consequently, before StopAndBuyBait()
executes even a single line of code, its stack frame already has two variables on it.
At this point, StopAndBuyBait()
optionally creates a local variable, wormsBought
. Notice that in the stack frame, parameters appear first, followed by the return address of the caller and then the local variables. When StopAndBuyBait()
determines what to return to GoFishing()
, it places this value in numberWorms
because numberWorms
is set to receive this return value.
The return address is a pointer to a specific place in memory that marks the continuation point in the code for GoFishing()
. So, the next step is to read the next processing instruction for GoFishing()
that comes after the return from StopAndBuyBait()
.
Because every variable lives somewhere in memory, every variable has an address. If you have a function that declares an integer variable called NumberOfPotholes
, then when your application calls this function, the computer will allocate space for NumberOfPotholes
somewhere in memory.
LISTING 8-1: Using the & Character to Obtain the Address of a Variable
#include <iostream>
using namespace std;
int main() {
int NumberOfPotholes = 532587;
cout << &NumberOfPotholes << endl;
return 0;
}
When you run this application, a hexadecimal number appears on the console. This number may or may not match ours, and it may or may not be the same each time you run the application. The result depends on exactly how the computer allocated your variable for you and the order in which it did things. This could be very different between versions of compilers. When you run Listing 8-1, you see something like the following (it varies with each run):
0x22ff74
Knowing the address of a variable doesn’t tell you about the variable content, but C++ programmers use addresses in other ways:
To declare a pointer variable, you need to specify the type of variable it will point to. Then you precede the variable’s name with an asterisk, as in the following:
int *ptr;
This line declares a variable that points to an integer. In other words, it can contain the address of an integer variable. And how do you grab the address of an integer variable? Easy! By using the &
notation! Thus, you can do something like this:
ptr = &NumberOfPotholes;
This line puts the address of the variable NumberOfPotholes
in the ptr
variable. Remember that ptr
doesn’t hold the number of potholes; rather, it holds the address of the variable called NumberOfPotholes
.
After you have a pointer variable holding another variable’s address, you can use the pointer to access the information in the other variable. That means you have two ways to get to the information in a variable: Use the variable name itself (such as NumberOfPotholes
), or use the pointer variable that points to it.
If you want to store the number 6087 in NumberOfPotholes
, you can do this:
NumberOfPotholes = 6087;
Or you can use the pointer. To use the pointer, you first declare it as follows:
ptr = &NumberOfPotholes;
Then, to change NumberOfPotholes
, you don’t just assign a value to it. Instead, you throw an asterisk in front of it, like so:
*ptr = 6087;
If ptr
points to NumberOfPotholes
, these two lines of code will have the same effect: Both will change the value to 6087. This process of sticking the asterisk before a pointer variable is called dereferencing the pointer. Look at the DereferencePointer
example, shown in Listing 8-2, which demonstrates all this.
LISTING 8-2: Modifying the Original Variable with a Pointer Variable
#include <iostream>
using namespace std;
int main() {
int NumberOfPotholes;
int *ptr;
ptr = &NumberOfPotholes;
*ptr = 6087;
cout << NumberOfPotholes << endl;
return 0;
}
In Listing 8-2, the first line of main()
declares an integer variable, and the second line declares a pointer to an integer. The next line takes the address of the integer variable and stores it in the pointer. Then the fourth line modifies the original integer by dereferencing the pointer. And just to make sure that the process worked, the next line prints the value of NumberOfPotholes
. When you run the application, you see the following output:
6087
You can also read the value of the original variable through the pointer. Look at the ReadPointer
example, shown in Listing 8-3. This code accesses the value of NumberOfPotholes
through the pointer variable, ptr
. When the code gets the value, it saves it in another variable called SaveForLater
.
LISTING 8-3: Accessing a Value through a Pointer
#include <iostream>
using namespace std;
int main() {
int NumberOfPotholes;
int *ptr = &NumberOfPotholes;
int SaveForLater;
*ptr = 6087;
SaveForLater = *ptr;
cout << SaveForLater << endl;
*ptr = 7000;
cout << *ptr << endl;
cout << SaveForLater << endl;
return 0;
}
When you run this application, you see the following output:
6087
7000
6087
Notice that the code changes the value through ptr
again — this time to 7000
. When you run the application, you can see that the value did indeed change, but the value in SaveForLater
remained the same. That’s because SaveForLater
is a separate variable, not connected to the other two. The other two, however, are connected to each other.
Pointer variables can point to any type, including strings. However, after you say that a variable points to a certain type, it can point to only that type. That is, as with any variable, you cannot change its type. The compiler won’t let you do it.
To create a pointer to a string, you simply make the type of the variable string *
. You can then set it equal to the address of a string variable. The StringPointer
example, shown in Listing 8-4, demonstrates this idea.
LISTING 8-4: Pointing to a String with Pointers
#include <iostream>
using namespace std;
int main() {
string GoodMovie;
string *ptrToString;
GoodMovie = "Best in Show";
ptrToString = &GoodMovie;
cout << *ptrToString << endl;
return 0;
}
In Listing 8-4, you see that the pointer named ptrToString
points to the variable named GoodMovie
. But when you want to use the pointer to access the string, you need to dereference the pointer by putting an asterisk (*
) in front of it. When you run this code, you see the results of the dereferenced pointer, which is the value of the GoodMovie
variable:
Best in Show
You can change the value of the string through the pointer, again by dereferencing it, as in the following code:
*ptrToString = "Galaxy Quest";
cout << GoodMovie << endl;
The code dereferences the pointer to set it equal to the string "GalaxyQuest"
. Then, to show that it truly changed, the code prints the GoodMovie
variable. The result of this code, when added at the end of Listing 8-4 (but prior to the return 0
), is
Galaxy Quest
You can also use the pointer to access the individual parts of the string, as shown in the StringPointer2
example in Listing 8-5.
LISTING 8-5: Using Pointers to Point to a String
#include <iostream>
using namespace std;
int main() {
string AMovie;
string *ptrToString;
AMovie = "L.A. Confidential";
ptrToString = &AMovie;
for (unsigned i = 0; i < AMovie.length(); i++) {
cout << (*ptrToString)[i] << " ";
}
cout << endl;
return 0;
}
When you run this application, you see the letters of the movie appear with spaces between them, as in
L . A . C o n f i d e n t i a l
This application loops through the entire string, character by character. The string’s length()
function tells how many characters are in the string. The code inside the loop grabs the individual characters and prints them with a space after each.
Notice that i
is of type unsigned
rather than int
. The length()
function returns an unsigned
value rather than an int
value, which makes sense because a string can’t have a negative length. If you try to use an int
for i
, the compiler displays the following warning:
warning: comparison between signed and unsigned integer
The application still runs, but you need to use the correct data types for loop variables. Otherwise, when the loop value increases over the amount that the loop variable can support, the application will fail. Trying to find such an error can prove frustrating even for the best developers. It’s important to not ignore warnings even if they appear harmless.
for (unsigned i = 0; i < (*ptrToString).length(); i++) {
cout << (*ptrToString)[i] << " ";
}
When you create a pointer variable, you must specify the type of data it points to. After that, you cannot change the type of data it points to, but you can change what it points to. For example, if you have a pointer to an integer, you can make it point to the integer variable called ExpensiveComputer
. Then, later, in the same application, you can make it point to the integer variable called CheapComputer
. Listing 8-6 demonstrates this technique in the ChangePointer
example.
LISTING 8-6: Using Pointers to Point to Something Else and Back Again
#include <iostream>
using namespace std;
int main() {
int ExpensiveComputer;
int CheapComputer;
int *ptrToComp;
ptrToComp = &ExpensiveComputer;
*ptrToComp = 2000;
cout << *ptrToComp << endl;
ptrToComp = &CheapComputer;
*ptrToComp = 500;
cout << *ptrToComp << endl;
ptrToComp = &ExpensiveComputer;
cout << *ptrToComp << endl;
return 0;
}
This code starts out by initializing all the goodies involved — two integers and a pointer to an integer.
Next, the code points the pointer to ExpensiveComputer
and uses the pointer to put 2000
inside ExpensiveComputer
. It then writes the contents of ExpensiveComputer
, again by using the pointer.
Then the code changes what the pointer points to. To do this, you set the pointer to the address of a different variable, &CheapComputers
. The next line stores 500
in CheapComputers
. And, again, you print it.
Now, just to drive home the point, in case the computer isn’t listening, you then point the pointer back to the original variable, ExpensiveComputer
. But you don’t store anything in it. This time, you simply print the cost of this high-powered supermachine. You do this again by dereferencing the pointer. And when you run the application, you see that ExpensiveComputer
still has 2000
in it, which is what was originally put in it. This means that after you point the pointer to something else and do some finagling, the original variable remains unchanged.
This section contains tips on using pointer variables. You can declare two pointer variables of the same type by putting them together in a single statement, as you can with regular variables. However, you must precede each one with an asterisk, as in the following line:
int *ptrOne, *ptrTwo;
int *ptrOne, Confused;
Here, Confused
is not a pointer to an integer; rather, it’s just an integer. Beware!
int* ptrOne;
However, this approach makes it easy to leave out the asterisks for any pointer variables that follow.
It isn’t possible to predict some kinds of memory use in your application, but the requirements aren’t known when you write the code. For example, streaming data from the Internet or creating new records in a database are both examples of unpredictable memory use. When working with unpredictable memory requirements, you allocate (request memory) and deallocate (release the memory you requested) as needed in a process called dynamic memory management. You use the heap, an area of unallocated memory, to perform dynamic memory management.
Most modern programming languages provide a means for managing memory for you. The reason for using this strategy is that older memory management techniques are error prone. You often see these common memory errors using older methods:
Consequently, most modern languages simply allow you to create and delete variables using one simple approach, and a process called garbage collection (the freeing of unused memory) occurs in the background. C++ is moving in this direction. However, the transition is taking some time.
Up to this point, you allocated memory using various approaches including the new
keyword. Using new
simply meant that you needed memory for a specific purpose. The new
keyword is deprecated in C++ 20 and will disappear altogether in C++ 23. The following sections begin with two examples of using new
because you see new
used in all current existing code of any complexity at this point. The remaining three sections tell you about the updated C++ 20 method of managing memory.
To declare a storage bin on the heap using existing methods, first you need to set up a variable that will help you keep track of the storage bin. This variable must be a pointer variable.
Suppose that you already have an integer declared out on the heap somewhere. (You see how to do that in the next paragraph.) Oddly enough, such variables don’t have names. Just think of it as an integer on the heap. Then, with the integer variable, you could have a second variable. This second variable is not on the heap, and it’s a pointer holding the address of the integer variable. So if you want to access the integer variable, you do so by dereferencing (looking at the address of) the pointer variable.
To allocate memory on the heap, you need to do two things: First, declare a pointer variable. Second, call a function named new
. The new
function is a little different from other functions in that you don’t put parentheses around its parameter. For this reason, it’s actually an operator. Other operators are +
and –
and are for adding and subtracting integers. These other operators behave similarly to functions, but you don’t use parentheses.
To use the new
operator, you specify the type of variable you want to create. For example, the following line creates a new integer variable:
int *somewhere = new int;
After the computer creates the new integer variable on the heap, it stores the address of the integer variable in somewhere
. And that makes sense: somewhere
is a pointer to an integer, so it’s prefaced by the *
(pointer) operator. Thus, somewhere
holds the address of an integer variable. The UseNew
example, shown in Listing 8-7, demonstrates how pointers work when using new
.
LISTING 8-7: Allocating Memory by Using new
#include <iostream>
using namespace std;
int main() {
int *ptr = new int;
*ptr = 10;
cout << *ptr << endl;
cout << ptr << endl;
return 0;
}
When you run this application, you see this sweet and simple output (the second value will change each time you run the example):
10
0x73af10
In this application, you first allocate a pointer variable, which you call ptr
. Then you call new
with an int
type, which returns a pointer to an integer. You save that return value in the ptr
variable.
Then you start doing your magic on it. Okay, so it’s not all that magical, but you save a 10
in the memory that ptr
points to. And then you print the value stored in the memory that ptr
points to.
To see for yourself that ptr
is pointing to a memory location and not the actual value of 10
, the code also prints ptr
without dereferencing it (using the *
operator). The output is a hexadecimal value such as 0x9caef0
, but this output will change each time because the memory allocation occurs in a different location on the heap each time.
As you can see, ptr
contains the address of the memory allocated by the new
operator. But unlike regular variables, the variable pointed at by ptr
doesn’t have a name. And because it doesn’t have a name, the only way you can access it is through the pointer. It’s kind of like an anonymous author with a publicist. If you want to send fan mail to the author, you have to go through the publicist. Here, the only way to reach this unnamed but famous variable is through the pointer.
But this doesn’t mean that the variable has a secret name such as BlueCheese
and that, if you dig deep enough, you might discover it; it just means that the variable has no name. Sorry.
By using pointers to access memory on the heap, you can take advantage of many interesting C++ features. For example, you can use pointers along with something called an array. An array (as described in Book 5, Chapter 1) is simply a large storage bin that has multiple slots, each of which holds one item. If you set up an array that holds pointers, you can store all these pointers without having to name them individually. And these pointers can point to complex things, called objects. (Book 2, Chapter 1 covers objects and Book 2, Chapter 2 discusses arrays.) You could then pass all these variables (which could be quite large, if they’re strings) to a function by passing only the array, not the strings themselves. That step saves memory on the stack.
In addition to objects and arrays, you can have a function allocate memory and return a variable pointing to that memory. Then, when you get the variable back from the function, you can use it, and when you finish with the variable, delete it (freeing the memory). Finally, you can pass a pointer into a function. When you do so, the function can actually modify the data the pointer references for you. (See “Passing Pointer Variables to Functions” and “Returning Pointer Variables from Functions,” later in this chapter, for details.)
When you call new
, you can provide an initial value for the memory you are allocating. For example, when allocating a new integer, you can, in one swoop, also store the number 10
in the integer. The Initializer
, example shown in Listing 8-8, demonstrates how to do this.
LISTING 8-8: Putting a Value in Parentheses to Initialize Memory That You Allocate
#include <iostream>
using namespace std;
int main() {
int *ptr = new int(10);
cout << *ptr << endl;
return 0;
}
This code calls new
, but also provides a number in parentheses. That number is put in the memory initially, instead of being assigned to it later. This line of code is equivalent to the following two lines of code:
int *ptr = new int;
*ptr = 10;
When you allocate memory on the heap by calling the new
operator and you’re finished using the memory, you need to let the computer know, regardless of whether it’s just a little bit of memory or a lot. The computer doesn’t look ahead into your code to find out whether you’re still going to use the memory. So in your code, when you are finished with the memory, you free the memory.
The way you free the memory is by calling the delete
operator and passing the name of the pointer:
delete MyPointer;
This line would appear after you’re finished using a pointer that you allocated by using new
. (Like the new
operator, delete
is also an operator and does not require parentheses around the parameter.)
The FreePointer
example, shown in Listing 8-9, provides a complete demonstration of allocating a pointer, using it, and then freeing it. Note the use of the replace()
method, which first appears in the “Replacing parts of a string” section of Book 1 Chapter 6. You use the arrow operator (->
) to access this string
method of phrase
. The “Using classes and raw pointers” section of Book 2 Chapter 1 describes the arrow operator in more detail.
LISTING 8-9: Using delete to Clean Up Your Pointers
#include <iostream>
using namespace std;
int main() {
string *phrase =
new string("All presidents are cool!!!");
cout << *phrase << endl;
(*phrase)[20] = 'r';
phrase->replace(22, 4, "oked");
cout << *phrase << endl;
delete phrase;
return 0;
}
When you run this application, you see the following output:
All presidents are cool!!!
All presidents are crooked
This code allocates a new string and initializes it, saving its address in the pointer variable called phrase
. The code outputs the phrase, manipulates it, and then writes it again. Finally, the code frees the memory used by the phrase.
Whenever you free a pointer, a good habit is to set the pointer to the value 0
or nullptr
(when using C++ 11 or above). Then, whenever you use a pointer, first check whether it’s equal to 0
(or nullptr
) and use it only if it’s not 0
. This strategy always works because the computer will never allocate memory for you at address 0
. So the number 0
can be reserved to mean I point to nothing at all.
The following code sample shows how to use this strategy. First, this code frees the pointer and then clears it by setting it to 0
:
delete ptrToSomething;
ptrToSomething = 0;
The reason to use nullptr
in place of 0
when you can is that nullptr
is clearer — it says precisely what you’re doing to the pointer. This code checks whether the pointer is not 0
before using it:
ptrToComp = new int;
*ptrToComp = 10;
if (ptrToComp != 0) {
cout << *ptrToComp << endl;
}
As mentioned previously in the chapter, smart pointers are the direction that C++ is taking, so you need to use them in all new application development. The reason is simple: Using smart pointers reduces the amount of code you must create, reduces errors, makes applications more efficient, and virtually eliminates many common application issues, such as memory leaks. The following sections offer an overview of smart pointers. Most of the code will run with C++ 17, but some of the items are C++ 20 specific.
Smart pointers do a lot of work for you when it comes to memory management, so you should use them in new projects and when converting old projects. The biggest advantage of smart pointers is that they automatically deallocate resources for you, so you don’t encounter problems like memory leaks in your applications. However, they can do a lot more for you by enforcing good programming practices through the compiler. No longer can you create code that’s easy to crash because you’re attempting to use a pointer that doesn’t point anywhere. You also gain access to unique functions and operators that help you better understand how memory is used.
Normally you use unique_ptr
when working in an environment where you don’t need to copy pointers. Using unique_ptr
makes your code significantly safer and more bulletproof. The UniquePtr
example, shown in Listing 8-10, gets you started on using unique_ptr
.
LISTING 8-10: Using a unique_ptr to Perform Common Tasks
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> ptr1(new int());
*ptr1 = 100;
cout << "ptr1 value: " << *ptr1 << endl;
int myValue = 42;
unique_ptr<int> ptr2(&myValue);
cout << "ptr2 value: " << *ptr2 << endl;
unique_ptr<int> ptr3 = make_unique<int>(99);
cout << "ptr3 value: " << *ptr3 << endl;
cout << "ptr3 address: " << ptr3.get() << endl;
unique_ptr<int> ptr4;
ptr4 = move(ptr3);
if (ptr3 == nullptr) {
cout << "ptr3 is nullptr." << endl;
}
cout << "ptr4 value: " << *ptr4 << endl;
cout << "ptr4 address: " << ptr4.get() << endl;
return 0;
}
The example shows three ways to create a unique_ptr
:
new
operator.make_unique()
function.In all three cases, you get a unique_ptr
with the value you specify. Notice that you must specify the pointer type using <int>
(for an int
value). As with other pointers, you can’t really create a generic pointer that can point to anything.
As previously mentioned, you can’t make one unique_ptr
equal to another unique_ptr
. However, you can use the move()
function to move the address of one unique_ptr
to another unique_ptr
. The swap()
function simply swaps addresses between two pointers.
This example also shows the use of nullptr
. As you can see, using nullptr
is clearer than using 0
in your code. Here is the output from this example:
ptr1 value: 100
ptr2 value: 42
ptr3 value: 99
ptr3 address: 0x5daf28
ptr3 is nullptr.
ptr4 value: 99
ptr4 address: 0x5daf28
To really understand unique_ptr
versus shared_ptr
, you need to compare usage side by side. The SharedPtr
example, shown in Listing 8-11, demonstrates some differences that you need to consider when choosing between the two pointer objects.
LISTING 8-11: Using a shared_ptr for Copying
#include <iostream>
#include <memory>
using namespace std;
int main() {
int myValue = 42;
shared_ptr<int> ptr1(new int(myValue));
cout << "ptr1 value: " << *ptr1 << endl;
cout << "ptr1 use count: " << ptr1.use_count()
<< endl;
shared_ptr<int> ptr2 = ptr1;
cout << "ptr2 value: " << *ptr2 << endl;
cout << "ptr1 address: " << ptr1 << endl;
cout << " ptr2 address: " << ptr2 << endl;
cout << "ptr1 use count: " << ptr1.use_count()
<< endl;
ptr2.reset();
cout << "ptr1 use count: " << ptr1.use_count()
<< endl;
ptr1.reset();
cout << "ptr1 use count: " << ptr1.use_count()
<< endl;
return 0;
}
When working with a shared_ptr
, you can make one pointer equal to another pointer, as this example shows. The code demonstrates that both ptr1
and ptr2
point to the same memory location and have the same value. Consequently, the resource (not the pointers) is shared between the two pointers.
To make it easier to determine how many references a resource has, you use the use_count()
function. Each additional reference increments the count so that you’re never in the dark as to how many references the resource has.
Of course, now you need some way to remove references when they’re no longer needed. To perform this task, you use reset()
. The code uses ptr2.reset()
to remove the second reference to myValue
. As shown in the following output, the use count decreases each time you reset()
a pointer.
ptr1 value: 42
ptr1 use count: 1
ptr2 value: 42
ptr1 address: 0x6caf08
ptr2 address: 0x6caf08
ptr1 use count: 2
ptr1 use count: 1
ptr1 use count: 0
An optional value is one that may or may not be there. For example, a caller may supply an int
value when calling your function, or may send nothing at all. In some cases, when an error occurs, the value may simply not exist. C++ developers have tried to come up with all sorts of solutions to the problem of values not being provided, but none of them is as good as using optional
. If a value doesn’t appear in the optional
object, it’s easy to check using nullopt
.
You may wonder why optional
appears in this chapter. After all, it should possibly appear in Book 1, Chapter 6 when working with functions. In many respects, optional
appears as a pointer because it supports many of the same features as unique_ptr
and shared_ptr
do. For example, you have access to the reset()
and swap()
functions, as described at https://en.cppreference.com/w/cpp/utility/optional
. It’s actually easier to understand optional
after you get to this point in the book, which is why it appears here.
The Optional
example, shown in Listing 8-12, demonstrates how to create a function that could receive a string, but then again, perhaps not. (Note that this example may not run in Code::Blocks because of problems in GCC. Currently, you must change the #include <optional>
to read #include <experimental/optional>
because the support is experimental. There are other necessary changes as well, which you can see in the OptionalExperimental
project in the downloadable source code.)
LISTING 8-12: Using optional to Avoid Instances of Nothing
#include <iostream>
#include <optional>
using namespace std;
void myFunction(optional<string> name = nullopt) {
if (name == nullopt) {
cout << "I wish I knew your name!" << endl;
} else {
cout << "Hello " << name.value() << "!" << endl;
}
}
int main() {
myFunction();
myFunction("Sarah");
return 0;
}
In this case, you see myFunction()
, which accepts nothing or a string
. If the caller sends nothing, then name
equals nullopt
. On the other hand, if the caller sends a string
, the code uses name.value()
to obtain the string
and print it onscreen. Note that you can’t access the string
directly but must call value()
instead. Here is the output from this example:
I wish I knew your name!
Hello Sarah!
One of the most important uses for pointers is this: If a pointer points to a variable, you can pass the pointer to a function, and the function can modify the original variable. This functionality lets you write functions that can actually modify the variables passed to them. Even though this section discusses raw pointers, the same techniques work with smart pointers.
Normally, when you call a function and you pass a few variables to the function, the computer just grabs the values out of the variables and passes those values. Take a close look at the VariablePointer
example, shown in Listing 8-13.
LISTING 8-13: A Function Cannot Change the Original Variables Passed into It
#include <iostream>
using namespace std;
void ChangesAreGood(int myparam) {
myparam += 10;
cout << "Inside the function:" << endl;
cout << myparam << endl;
}
int main() {
int mynumber = 30;
cout << "Before the function:" << endl;
cout << mynumber << endl;
ChangesAreGood(mynumber);
cout << "After the function:" << endl;
cout << mynumber << endl;
return 0;
}
Listing 8-13 includes a function called ChangesAreGood()
that modifies the parameter it receives. (It adds 10
to its parameter called myparam
.) It then prints the new value of the parameter.
The main()
function initializes an integer variable, mynumber
, to 30
and prints its value. It then calls the ChangesAreGood()
function, which changes its parameter. After coming back from the ChangesAreGood()
function, main()
prints the value again. When you run this application, you see the following output:
Before the function:
30
Inside the function:
40
After the function:
30
Before the function call, mynumber
is 30
. And after the function call, it’s still 30
. But the function added 10
to its parameter. This means that when the function modified its parameter, the original variable remains untouched. The two are separate entities. Only the value 30
went into the function. The actual variable did not. It stayed in main()
. But what if you write a function that you want to modify the original variable?
A pointer contains a number, which represents the address of a variable. If you pass this address into a function and the function stores that address into one of its own variables, its own variable also points to the same variable that the original pointer did. The pointer variable in main()
and the pointer variable in the function both point to the same variable because both pointers hold the same address.
That’s how you let a function modify data in a variable: You pass a pointer. But when you call a function, the process is easy because you don’t need to make a pointer variable. Instead, you can just call the function, putting an &
in front of the variable. Then you’re not passing the variable or its value — instead, you’re passing the address of the variable.
The VariablePointer2
example, shown in Listing 8-14, is a modified form of Listing 8-13; this time, the function actually manages to modify the original variable.
LISTING 8-14: Using Pointers to Modify a Variable Passed into a Function
#include <iostream>
using namespace std;
void ChangesAreGood(int *myparam) {
*myparam += 10;
cout << "Inside the function:" << endl;
cout << *myparam << endl;
}
int main() {
int mynumber = 30;
cout << "Before the function:" << endl;
cout << mynumber << endl;
ChangesAreGood(&mynumber);
cout << "After the function:" << endl;
cout << mynumber << endl;
return 0;
}
When you run this application, you see the following output:
Before the function:
30
Inside the function:
40
After the function:
40
Notice the important difference between this and the output from Listing 8-13: The final line of output is 40
, not 30
. The variable was modified by the function!
To understand how this happened, first look at main()
. The only difference in main()
is that it has an ampersand (&
) in front of the mynumber
argument in the call to ChangesAreGood()
. ChangesAreGood()
receives the address of mynumber
.
Now the function has some major changes. The function header takes a pointer rather than a number. You perform this task by adding an asterisk (*
) so that the parameter is a pointer variable. This pointer receives the address being passed into it. Thus, it points to the variable mynumber
. Therefore, any modifications made by dereferencing the pointer will change the original variable. The following line changes the original variable.
(*myparam) += 10;
Functions can return values, including pointers. To set up a function to return a pointer, specify the type followed by an asterisk at the beginning of the function header. The ReturnPointer
example, shown in Listing 8-15, demonstrates this technique. The function returns a pointer that is the result of a new
operation.
LISTING 8-15: Returning a Pointer from a String Involves Using an Asterisk in the Return Type
#include <iostream>
#include <sstream>
#include <stdlib.h>
using namespace std;
string *GetSecretCode() {
string *code = new string;
code->append("CR");
int randomnumber = rand();
ostringstream converter;
converter << randomnumber;
code->append(converter.str());
code->append("NQ");
return code;
}
int main() {
string *newcode;
for (int index = 0; index < 5; index++) {
newcode = GetSecretCode();
cout << *newcode << endl;
}
return 0;
}
The main()
function creates a pointer to a string
named newcode
. GetSecretCode()
returns a pointer to a string
, so newcode
and the function return value match. When you use newcode
, you must dereference it.
When you run this application, you see something like the following output:
CR41NQ
CR18467NQ
CR6334NQ
CR26500NQ
CR19169NQ