Chapter 8

Referring to Your Data Through Pointers

IN THIS CHAPTER

check Using two types of memory: the stack and the heap

check Accessing variable addresses through pointers

check Creating variables on the heap by using the new keyword

check 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.

Tip If any single topic in C++ programming is most important, it is the notion of pointers. Therefore, if you want to become a millionaire, read this chapter. Okay, so it may not make you a millionaire, but suggesting it could give you the incentive to master this chapter. Then you can become an ace programmer and make lots of money.

Remember You don’t have to type the source code for this chapter manually. In fact, using the downloadable source is a lot easier. You can find the source for this chapter in the \CPP_AIO4\BookI\Chapter08 folder of the downloadable source. See the Introduction for details on how to find these source files.

Understanding the Changes in Pointers for C++ 20

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.

Avoiding broken code

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:

  • Dynamic allocation: Allows an application to allocate more memory as needed
  • Runtime polymorphism: Allows an application to pass pointers that may point to different kinds of data at different times
  • Nullable references: Handles instances in which a pointer doesn’t point to anything
  • Avoiding copies: Uses a single copy of an object instead of creating multiple copies, which reduces the risk of errors

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++.

Considering the issues

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:

  • Creating a memory leak: By not deallocating the pointer so you can reuse the memory, the memory becomes inaccessible to the application. You could actually run out of memory despite having memory available. The memory becomes available again after the operating system frees it once the application terminates.
  • Using memory that hasn’t been initialized: The memory location could contain anything and if you act on the data in that memory location, your application will act oddly or simply crash.
  • Obtaining the wrong data: The application could point to the wrong location and you might not know it. This means that the application is using the wrong data, which could result in unanticipated output or data damage.

Writing cleaner and less bug-prone code

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:

  • Using smart pointers: Boost (explained in Book 7, Chapter 4) has provided access to smart pointers for a long time, and many developers use them because they make both dynamic allocation and runtime polymorphism easier to deal with. Using a smart pointer, such as 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.
  • Relying on optional pointers: C++ 17 introduced 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.
  • Passing objects around: A modern computer isn’t nearly as resource limited as those in the past were, so modern languages commonly pass objects around rather than create pointers to them. This solution addresses the need to eliminate unwanted copies. C++ 20 provides two solutions for this task, both of which rely on the idea of using the object 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:
    • Technical stuff 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/.

    • Technical stuff 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.

Heaping and Stacking the Variables

C++ applications use two kinds of memory:

  • Heap: A common area in memory where you can store global variables. This is where you also store objects and variables that you allocate from memory.
  • Stack: The area where the computer stores both function information and local value type variables for those functions. The stack also stores pointers to local object type variables for functions.

Remember Function storage is a little more complicated because each function gets its own little private area at the top of the stack. It is called a stack because it’s treated like a stack of papers: You can put something on the top of the stack, and you can take something off the top of the stack, but you can’t put anything in the middle or bottom. In addition, you can’t take anything from the middle or bottom. (You can, however, peek at the values from any place in the stack and change those values — it’s the memory block that isn’t removed.) The computer uses this stack to keep track of all your function calls.

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.

Schematic illustration of the two stack frames which are used for the example code.

FIGURE 8-1: The two stack frames used for the example code.

Remember From a stack perspective, the code begins by creating a stack frame for GoFishing(). On this stack frame, it creates a variable holding a pointer with the return address of the caller (which is unknown in this case). Adding a value to the stack is called pushing. GoFishing() creates two variables, baitMoney and numberWorms. From a stack perspective, because GoFishing() creates baitMoney first, it also appears first on the stack.

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.

Remember The application then starts to dismantle the StopAndBuyBait() stack frame by popping (removing) the values off the stack. It throws wormsBought away (if StopAndBuyBait() created it) because the application has already placed this value in numberWorms. The application saves the GoFishing() return address for later use. It then throws customerMoney away and removes the stack frame.

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().

Getting a variable’s address

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.

Remember If you want to find the address of the variable NumberOfPotholes, you simply throw an ampersand (&) in front of it. Listing 8-1 shows the VariableAddress example, which obtains the address of a variable and prints it.

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

Remember The output you see from this application is the address of the variable called NumberOfPotholes. In other words, that number is the hex version of the place where the NumberOfPotholes variable is stored in memory. The output is not the content of the variable or the content of the variable converted to hex; rather, it’s the address of the variable in hex.

Knowing the address of a variable doesn’t tell you about the variable content, but C++ programmers use addresses in other ways:

  • Modifying the variable content directly using what are called pointer variables. A pointer variable is just like any other variable except that it stores the address of another variable.
  • Performing any of the tasks mentioned in the “Avoiding broken code” section of the chapter.
  • Modifying values pointed at by the address indirectly using any of a number of math techniques.
  • Comparing entities such as objects based on their pointers.

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.

Tip You specify the type of pointer by the type of item it points to. If a pointer variable points to an integer, its type is pointer to integer. In C++ notation, its type is int * (with a space between them) or int* (no space); you are allowed to enter it with or without a space. If a pointer variable points to a string, its type is pointer to string, and notation for this type is string *.

Remember The ptr variable holds an address, but what’s at that address? That address is the location in memory of the storage bin known as NumberOfPotholes. Right at that spot in memory is the data stored in NumberOfPotholes.

Tip Think this pointer concept through carefully. If you have to, reread this section a few times until it’s locked in your head. Then meditate on it. Wake up in the night thinking about it. Call strangers on the telephone and chitchat about it. The more you understand pointers, the better off your programming career will be — and the more likely you are to make a million dollars.

Changing a variable by using a pointer

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.

Pointing at a string

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

Warning When you access the characters of the string through a pointer, you need to put parentheses around the asterisk and the pointer variable. Otherwise, the compiler gets confused and first tries to access the index in brackets with the variable name and afterward applies the asterisk. That’s backward, and it doesn’t make sense to the computer, so the compiler gives you an error message. But you can make it all better by using parentheses, as shown in Listing 8-5.

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.

Tip You can also change the individual characters in a string through a pointer. You can do this by using a line like (*ptrToString)[5] = ’X’;. Notice you still need to put parentheses around the variable name along with the dereferencing character.

Tip The length of a string is also available through the pointer. You can call the length() function by dereferencing the pointer, again with the carefully placed parentheses, such as in the following:

for (unsigned i = 0; i < (*ptrToString).length(); i++) {
cout << (*ptrToString)[i] << " ";
}

Pointing to something else

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.

Tip Be careful if you use one pointer to bounce around several different variables. It’s easy to lose track of which variable the pointer is pointing to.

Tips on pointer variables

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;

Warning If you try to declare multiple pointers on a single line but put an asterisk only before the first pointer, only that one will be a pointer. The rest will not be. This can cause serious headaches later because this line compiles fine:

int *ptrOne, Confused;

Here, Confused is not a pointer to an integer; rather, it’s just an integer. Beware!

Tip Some people like to put the asterisk immediately after the type, as in the following example, to emphasize the fact that the type is pointer to integer:

int* ptrOne;

However, this approach makes it easy to leave out the asterisks for any pointer variables that follow.

Creating New Raw Pointers

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:

  • Code tries to use the memory without allocating it first.
  • Memory remains allocated after use, creating a memory leak.
  • Uninitialized memory contains random data.

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.

Using new

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.

Remember When you call new, you get back a pointer. This pointer is of the type that you specify in your call to new. You can then store the pointer only in a pointer variable of the same type.

Tip When you use the new operator, the usual terminology is that you are allocating memory on the heap.

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.)

Using an initializer

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;

Technical stuff When you initialize a value in the new operator, the technical phrase for what you are doing is invoking a constructor. The reason is that the compiler adds a bunch of code to your application — code that operates behind the scenes. This code is the runtime library. The library includes a function that initializes an integer variable if you pass an initial value. The function that does this is known as a constructor. When you run it, you are invoking it. Thus, you are invoking the constructor. For more information on constructors, see Book 2, Chapter 1.

Freeing Raw Pointers

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.

Tip Although people usually say that you’re deleting the pointer or freeing the pointer, you’re actually freeing the memory that the pointer points to. The pointer can still be used for subsequent new operations.

Warning When you free memory, the memory becomes available for other tasks. However, immediately after the call to delete, the pointer still points to that particular memory location, even though the memory is free. Using the pointer without pointing it to something else causes errors. Therefore, don’t try to use the pointer after freeing the memory it points to until you set the pointer to point to something else through a call to new or by setting it to another variable.

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;
}

Warning Call delete only on memory that you allocated by using new. Although the Code::Blocks compiler doesn’t seem to complain when you delete a pointer that points to a regular variable, it serves no purpose to do so. You can free only memory on the heap, not local variables on the stack. In addition, you should avoid freeing the same pointer multiple times because doing so can create hard-to-find bugs; the application may have already reallocated that memory for some other purpose.

Warning An older method of freeing a pointer involves setting the pointer to NULL. Code::Blocks raises an error when you attempt to use NULL normally because NULL isn’t part of the standard and it’s considered outdated. However, you may have a lot of older code that uses NULL. In this case, you must add #include <cstddef> to your code to allow it to compile. However, it would be better to update the code to use either 0 or nullptr.

Working with Smart Pointers

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.

Creating smart pointers using std::unique_ptr and std::shared_ptr

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.

Remember This section discusses two smart pointer classes from an overview perspective: unique_ptr and shared_ptr. The main difference between them is that a unique_ptr is the only pointer that can point to a resource. If you attempt to copy a unique_ptr to another pointer, the compiler will complain. Using a unique_ptr keeps you from making copies that could cause problems in deallocating a resource. However, there are times when you actually do need to copy pointers, such as dealing with a multithreaded environment. In this case, you use a shared_ptr because you can copy a shared_ptr to another pointer. In fact, it even includes a function that tells you how many references currently exist to the resource. Whether you use unique_ptr or shared_ptr, both object types wrap a raw pointer in an object that performs all the management tasks for you.

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:

  • Use the new operator.
  • Create a variable and point to it.
  • Employ the 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.

Remember A unique_ptr provides you with a number of functions. Unlike most pointers, you can’t simply specify the pointer name and obtain its address because unique_ptr exercises stricter control over accessing the address information. You must use the get() function instead, as shown in the code.

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

Warning The important thing to remember about copying pointers is that copying a pointer only copies the pointer address, not the underlying reference. Consequently, if you copy a pointer to an array, there is still just one array, but now you have two references to that array. To create a copy of an array, you would need to create a second array of the same size and copy the data, index by index, from the first array to the second array.

Technical stuff Some significant differences exist between the C++ 17 and the C++ 20 versions of the smart pointer classes. One of the most important changes from a coding perspective is that C++ 20 relies on the spaceship operator (see the “Considering the new spaceship operator” sidebar of Book 1, Chapter 5 for details) in place of the !=, <, <=, >, and >= operators. If you try to use these operators in a C++ 20 application, you see an error message. See https://en.cppreference.com/w/cpp/memory/unique_ptr and https://en.cppreference.com/w/cpp/memory/shared_ptr for other version differences that could cause errors when updating your code.

Defining nullable values using std::optional and std::nullopt

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!

Warning You might be tempted to think that nullopt somehow equals nullptr. However, this isn’t the case. If you try to replace the nullopt check in Listing 8-12 with (ptr1 == nullptr), the compiler will complain loudly that you’re using the wrong data type.

Passing Pointer Variables to Functions

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;

Technical stuff The ChangesAreGood() function in Listing 8-14 no longer modifies its own parameter. The parameter holds the address of the original mynumber variable, and that never changes. Throughout the function, the pointer variable myparam holds the mynumber address. And any changes the function performs are on the dereferenced variable, which is mynumber.

Returning Pointer Variables from Functions

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

Warning Never return from a function the address of a local variable in the function. The local variables live in the stack space allocated for the function, not in the heap. When the function is finished, the computer frees the stack space used for the function, making room for the next function call. If you try this, the variables will be okay for a while, but after enough function calls follow, the variable’s data will get overwritten.

Technical stuff Just as the parameters to a function are normally values, a function normally returns a value. In the case of returning a pointer, the function is still returning just a value — it is returning the value of the pointer, which is a number representing an address.