Pointer Basics

Program variables are each stored at a difference address within our computer’s memory. Pointers can be used to access the contents of variables stored at specific memory addresses.

Table of Contents

  1. The Address-Of Operator
  2. The Indirection Operator
  3. What are Pointers?
  4. Defining and Initializing Pointers
  5. Redefining a Pointer
  6. Example: C-Array Pointer Navigation
  7. Pointers and Const
  8. Uninitialized Pointers
  9. Null Pointers
  10. Guarding Against Dereferencing a Null Pointer
  11. NULL and 0 for Null Pointers
  12. Pointers vs References
  13. Further Reading

The Address-Of Operator

When we store data in a variable, that data gets stored in our computer’s Random Access Memory (RAM).

We can access the memory address of any variable using the address-of operator &:

The Indirection Operator

What can we do with the memory address of a variable? Not much without the indirection operator *.

Using this operator, we can access the data stored at a particular memory address. This is sometimes called dereferencing.

What are Pointers?

The address-of and indirection operators aren’t too exciting on their own, but they are crucial for understand the concept of pointers.

A pointer is a type of variable used to store the memory address of another variable. In this way, a pointer “points to” another variable.

When people talk about C or C++ being confusing this opinion usually has something to do with pointers (and manual memory management).

🎵 Note:

The address-of operator & returns a pointer, not a raw number for the memory address.

Defining and Initializing Pointers

We declare pointers by placing an asterisk after the data type in the declaration.

  // Create an integer variable:
  int wholeNumber{ 42 };
  // Create a pointer to the integer variable:
  int* numberPointer{ &wholeNumber };
  // Access the value 42 by way of the variable:
  std::cout << " wholeNumber: " << wholeNumber << "\n";
  // Access the value 42 by dereferencing the pointer:
  std::cout << " *numberPointer: " << *numberPointer << "\n";
  // Change the value of wholeNumber using the pointer:
  (*numberPointer)++; // wholeNumber is now 43

⚡ Warning:

The pointer definition use of an asterisk is different from the indirection operator.

Redefining a Pointer

After a pointer has been defined and initialized we can also change what it points to.

Example: C-Array Pointer Navigation

Many collections in C++ are implemented using pointers. Look back over our module on iterators and you’ll see how similar pointers are to iterators.

C-style arrays are also implemented using pointers and we can use what is call pointer arithmetic to access elements within an array.

⚡ Warning:

Pointer arithmetic can be dangerous if you point beyond array boundaries.

Pointers and Const

The concept of const can be applied to pointers in a few different ways:

  • Regular pointers cannot point to const variables:
  const int answer{42};
  int* answerPointer{&answer}; // COMPILE ERROR: Regular pointers can't point to const variables.
  • Pointers can be made to point to const variables, but the pointer can be reassigned:
  const int answer1{42};
  const int answer2{999};
  const int* answerPointer{&answer1}; // Okay! Note: const comes first.
  answerPointer = &answer2;           // Weird, but also fine!
  (*answerPointer) = 12;              // COMPILE ERROR: Cannot change a const value.
  • Pointers can also be made const, meaning they cannot be changed after initialization:
  int answer1{42};
  int answer2{999};
  int* const answerPointer{&answer1}; // Note: const comes after type.
  answerPointer = &answer2;           // COMPILE ERROR: Const pointers cannot be reassigned.

Uninitialized Pointers

Unassigned pointers, sometimes called wild pointers contain what is known as a garbage address.

  double e{2.71828};
  double* validPointer{&e}; // Points to the memory address of the 'e' variable.
  double* garbagePointer; // Uninitialized. Contains a garbage address.

💡 Best Practice:

Dereferencing a wild pointer is undefined behaviour and should be avoided.

Null Pointers

There is a special literal value nullptr we can assign to pointers to indicate that they are uninitialized.

  int* nullPointer1{nullptr}; // Manually made null using the nullptr literal.
  int* nullPointer2{};        // An empty initializer will also create null pointers.

Previously assigned pointers can also be made null.

  double e{2.71828};
  double* validPointer{&e}; // Points to the memory address of the 'e' variable.
  validPointer = nullptr;   // Previously valid pointers can be made null.

💡 Best Practice:

Uninitialized pointers should always be explicitly made into null pointers.

Guarding Against Dereferencing a Null Pointer

Dereferencing a null pointer is undefined behaviour.

    int* pointer{}; // Creates a null pointer
    std::cout << *pointer; // UNDEFINED BEHAVIOUR!

As such, we should always guard pointer access with a boolean test.

    // METHOD #1:
    if (pointer != nullptr) {
      // pointer isn't null, so we can access it:
      std::cout << *pointer;
    }
    // METHOD #2:
    if (pointer) { // Non-null pointers are "truthy" while nullptr is "falsey".
      // pointer isn't null, so we can access it:
      std::cout << *pointer;
    }

⚡ Warning:

Pointers that evaluates as true aren’t guaranteed to point to a valid memory location.

NULL and 0 for Null Pointers

In legacy code you will often see the number 0 or the preprocessor macro NULL used in place of nullptr. Subtle bugs can be triggered when using 0 or NULL.

💡 Best Practice:

Always use nullptr when using C++ 11 or greater, even when updating legacy code.

Pointers vs References

At this point you might be wondering why C++ includes both pointers and references when they appear to offer nearly identical functionality.

The short answer: C++ inherited pointers from C and references were added later.

References should be preferred over pointers because:

  • Reference syntax is cleaner and pointer syntax. (No need for the indirection and address-of operators.)
  • References are safer than pointers as they must always refer to a valid variable. (Unlike pointers there is no such thing as an uninitialized or null reference.)

🎵 Note:

In legacy code (or in Unreal Engine C++) pointers are everywhere and cannot be avoided.

Further Reading

The learncpp.com website goes into great detail on all thing pointer related, specifically their sections: