Pointers

C++ stands out from many other programming languages because of “pointers,” which allow “low-level” (close to computer hardware) access to your computer’s memory.

Note: This is usually a challenging topic to wrap your head around at first, so it’s good to take your time reading the guide below (and testing out the sample programs). After programming more and more, you’ll start to see how powerful pointers can be.

Let’s begin by going over exactly what pointers are and how they can be used. Then, I’ll show you when they can useful in our code.

What are pointers?

A pointer is the “address” of a variable in your computer’s memory (RAM). Every single variable you create in C++ is assigned its own address so that it can easily be accessed later, just like how your home has its own street address.

These memory addresses are generally represented using hexadecimal (base 16) numbers, so they look something like this: 0x61fe14 .

Each address in your computer’s memory also has some value associated with it. For example, the address 0x61fe14 may be associated with the number 5 if a variable at that address is equal to 5. This is analogous to every street address generally being associated with some building.

Here’s a visualization of what a chunk of your computer’s memory may look like:

(You should see that each character in a string actually has its own memory address.)

How are pointers used?

The code below creates a pointer (called “pointerToNumber“) to the variable “number“. This means that “pointerToNumber” is equal to the memory address of “number“.

#include <iostream>

int main() {
    int number = 5;
    int *pointerToNumber = &number;
    std::cout << "Number: " << number << "\n";
    std::cout << "Memory address of number: " << pointerToNumber << "\n";
    return 0;
}

For me, the value of pointerToNumber is shown to be 0x61fe14, but it will likely be different for you because different computers can choose to use different memory addresses at different times.

Now let’s talk about the use of the asterisk (*) and ampersand (&) in the code above.

  • Asterisks between a variable’s data type and name mean that that variable is a pointer. Note that int* pointerToNumber and int * pointerToNumber would also work. The data type of a pointer (int in the code above) represents the data type of the variable the pointer points to. In other words, “pointerToNumber” is an int because it points to “number“, which is an int.
  • Ampersands before a variable’s name represent the “address-of” operator, which is used to get the memory address of a variable. Therefore, in the code above, pointerToNumber is set equal to the address of number.
Dereferencing Operator

To get the value at a memory address (to “dereference” the address), we can use the dereferencing operator. This can be thought of as the opposite of the address-of operator; rather than going from value to address, we go from address to value.

The dereferencing operator can be a bit confusing because it is also an asterisk, meaning that the asterisk can be used both to create a pointer and to dereference an address. Although the same symbol is used for these two processes, they are not at all related.

The code below shows how the operator’s used:

#include <iostream>

int main() {
    int number = 5;
    int *pointerToNumber = &number;
    int dereferencedAddress = *pointerToNumber;
    std::cout << "Number: " << number << "\n";
    std::cout << "Memory address of number: " << pointerToNumber << "\n";
    std::cout << "Number: " << dereferencedAddress << "\n";
    return 0;
}

The dereferencing operator works by putting an asterisk before the name of a pointer variable (because a pointer just represents a memory address). By doing so, you get the value of the variable the pointer points to.

What’s the point?

One of the hardest things about pointers is understanding why they’re useful at all. When first learning C++, it definitely took me a little while to see the point of pointers. Here, I’ll try to show you a couple ways they can be used.

Pass by Pointer

Remember when I mentioned “pass by value” in C++ functions? Here’s the code for recap:

#include <iostream>

void addFive(int number) {
    number += 5;
}

int main() {
    int number = 4;
    std::cout << "Number before adding 5: " << number << "\n";
    addFive(number);
    std::cout << "Number after adding 5: " << number << "\n";
    return 0;
}

With this program, the number variable is not modified by the addFive function because only the value of four is passed to the function (just a copy of the number variable).

We can use pointers to allow functions to actually modify their arguments. This can work in the code above by changing the number parameter of the addFive function to a pointer to a number. That way, the function can modify the value at the number’s address, rather than just modifying a copy of the number 4. Here’s how this would look in code:

#include <iostream>

void addFive(int *numberPointer) {
    *numberPointer += 5;
}

int main() {
    int number = 4;
    std::cout << "Number before adding 5: " << number << "\n";
    addFive(&number);
    std::cout << "Number after adding 5: " << number << "\n";
    return 0;
}

Let’s walk through this code:

  • The addFive function’s parameter is now a pointer to a number, meaning that the memory address of a variable is passed to the function.
  • Inside the addFive function, we add 5 to the dereferenced number pointer. In other words, the value at the address represented by numberPointer is increased by 5.
  • In the main function, we pass the address of the number variable to the addFive function. The address of number represents an int pointer because number is of an int data type.

After making these changes, the “Number after adding 5” printed to the screen is 9, so the number variable was successfully modified by the addFive function.

Improved Performance

When passing variables to a function by value, I said that a copy of the variable passed is made for the function to use. This process of copying variables can often be inefficient.

Let’s say we have a string containing a lot of text. Remember how each character in a string has its own memory address? This means that a large string can take up a lot of memory space.

If we ever want to use this large string in a function, passing the string by value to the function would be inefficient. Doing so would cause the computer to create a copy of every single character in the string, thus taking up double the amount of memory needed for the string. Here’s what this could look like:

#include <iostream>
#include <string>

void printString(std::string str) {
    std::cout << str << "\n";
}

int main() {
    std::string bigString = "According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off the ground. The bee, of course, flies anyway because bees don't care what humans think is impossible. Yellow, black. Yellow, black. Yellow, black. Yellow, black. Ooh, black and yellow! Let's shake it up a little. Barry! Breakfast is ready! Coming! Hang on a second. Hello? - Barry? - Adam? - Can you believe this is happening? - I can't. I'll pick you up. Looking sharp. Use the stairs. Your father paid good money for those. Sorry. I'm excited. Here's the graduate. We're very proud of you, son. A perfect report card, all B's. Very proud. Ma! I got a thing going here. - You got lint on your fuzz. - Ow! That's me! - Wave to us! We'll be in row 118,000. - Bye! Barry, I told you, stop flying in the house! - Hey, Adam. - Hey, Barry. - Is that fuzz gel? - A little. Special day, graduation. Never thought I'd make it. Three days grade school, three days high school. Those were awkward. Three days college. I'm glad I took a day and hitchhiked around the hive. You did come back different. - Hi, Barry. - Artie, growing a mustache? Looks good. - Hear about Frankie? - Yeah. - You going to the funeral? - No, I'm not going. Everybody knows, sting someone, you die. Don't waste it on a squirrel. Such a hothead.";
    printString(bigString);
    return 0;
}

On the other hand, by passing the string by pointer to the function, we’d only need to pass a single memory address to the function. This would then involve no unnecessary copying of the large string.

Here’s how passing the string by pointer would look:

#include <iostream>
#include <string>

void printString(std::string *str) {
    std::cout << *str << "\n";
}

int main() {
    std::string bigString = "According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off the ground. The bee, of course, flies anyway because bees don't care what humans think is impossible. Yellow, black. Yellow, black. Yellow, black. Yellow, black. Ooh, black and yellow! Let's shake it up a little. Barry! Breakfast is ready! Coming! Hang on a second. Hello? - Barry? - Adam? - Can you believe this is happening? - I can't. I'll pick you up. Looking sharp. Use the stairs. Your father paid good money for those. Sorry. I'm excited. Here's the graduate. We're very proud of you, son. A perfect report card, all B's. Very proud. Ma! I got a thing going here. - You got lint on your fuzz. - Ow! That's me! - Wave to us! We'll be in row 118,000. - Bye! Barry, I told you, stop flying in the house! - Hey, Adam. - Hey, Barry. - Is that fuzz gel? - A little. Special day, graduation. Never thought I'd make it. Three days grade school, three days high school. Those were awkward. Three days college. I'm glad I took a day and hitchhiked around the hive. You did come back different. - Hi, Barry. - Artie, growing a mustache? Looks good. - Hear about Frankie? - Yeah. - You going to the funeral? - No, I'm not going. Everybody knows, sting someone, you die. Don't waste it on a squirrel. Such a hothead.";
    printString(&bigString);
    return 0;
}

Later on, we’ll use pointers a lot for passing things like robot motors and sensors to functions because these also can take up a large amount of memory. I’ll show how this can work when going over “classes” later on.

As a last note on passing by pointers: You should only pass pointers to functions if there’s a reason to do so. For built-in data types like int, float, char, bool, etc., passing by pointer would be no more efficient than passing by value because these data types don’t take up a large amount of memory (only 1-8 bytes, depending on the data type). Because pointers make code harder to read, they should not be overused.

Stack vs. Heap

When variables are created in C++, they are placed on a “stack” that contains all variables accessible in the current scope (review the “Variable Scoping” section from Functions for a recap on variable scoping). When a variable goes out of scope (e.g. at the end of a function), it is “popped” (removed) from the stack.

This is important because it allows for memory to automatically be cleared when there are variables we can no longer use (since they’re out of scope). Having those variables still in memory would waste memory space.

It turns out that there’s actually another place in memory where variables can be stored: the “heap.” When variables are stored in the heap, they aren’t automatically removed from memory when they go out of scope. This can sometimes be useful if you need a variable to last in memory longer than its current scope.

To put variables on the heap, we can use the word “new“. Because variables on the heap are not automatically deleted, they must be deleted using the word “delete“. This is shown below:

#include <iostream>

int main() {
    float *numberPointer = new float(5.64);
    std::cout << *numberPointer << "\n";
    delete numberPointer;
    return 0;
}

With the code above, the float of value 5.64 is placed on the heap, printed to the screen, and then deleted from the heap. You should see that, when using new, we get the pointer of the value we added to the heap. In other words, the numberPointer variable stores the memory address of the number 5.64 in the heap. By dereferencing numberPointer, we get the number 5.64.

Memory Leaks

Because variables on the heap need to be manually deleted, it’s best to use the heap only when absolutely necessary (which often isn’t the case). Forgetting to delete a heap variable can result in a memory leak, in which a variable is stored on the heap until the program finishes executing. In a memory leak, access to the pointer of the value stored on the heap is lost, so deleting the value from the heap becomes impossible:

void memoryLeak() {
    float *numberPointer = new float(5.64);
}

int main() {
    memoryLeak();
    return 0;
}

Here, the numberPointer pointer goes out of scope when the memoryLeak function ends. Therefore, it’s impossible to delete 5.64 from the heap (since we no longer have access to the memory address of 5.64).

In the case of the program above, the memory leak is acceptable because the program ends (causing 5.64 to automatically be deleted) just after 5.64 is added to the heap. However, if multiple memory leaks occur throughout the course of the program (which may run for minutes, which is the case for our robot’s program), the available memory for your program can become severely limited.

tl;dr Don’t use the heap if you don’t need to!

Constant Pointers

If you recall from Variables and Basic Data Types, a normal variable declared as const (e.g. “const int x = 5;“) cannot be modified. When using pointers, there are actually four possible ways of using the keyword const.

Not at All
int main() {
    int x = 5;
    int *pointer = &x;
    
    int y = 10;
    pointer = &y; //Valid

    *pointer = 25; //Valid

    return 0;
}

Here, the memory address that the pointer “pointer” points to can be changed. Additionally, the value at the memory address pointed to can be changed.

Constant Value
int main() {
    int x = 5;
    const int *pointer = &x;
    
    int y = 10;
    pointer = &y; //Valid

    *pointer = 25; //Invalid

    return 0;
}

Here, only the memory address that the pointer “pointer” points to can be changed.

Constant Pointer
int main() {
    int x = 5;
    int * const pointer = &x;
    
    int y = 10;
    pointer = &y; //Invalid

    *pointer = 25; //Valid

    return 0;
}

Here, only the value at the memory address pointed to can be changed.

Constant Pointer and Value
int main() {
    int x = 5;
    const int * const pointer = &x;
    
    int y = 10;
    pointer = &y; //Invalid

    *pointer = 25; //Invalid

    return 0;
}

Here, the memory address that the pointer “pointer” points to cannot be changed. Additionally, the value at the memory address pointed to cannot be changed.