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 &:

#include <iostream> // std::cout
#include <string>   // std::string

int main() {
  int wholeNumber{ 42 };
  float decimalNumber{ 3.14159 };
  std::string name{ "Wally Glutton" };

  std::cout << "The number " << wholeNumber 
            << " is stored at memory address " 
            << &wholeNumber << ".\n";

  std::cout << "The number " << decimalNumber 
            << " is stored at memory address " 
            << &decimalNumber << ".\n";

  std::cout << "The string " << name 
            << " is stored at memory address " 
            << &name << ".\n";
}

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.

#include <iostream>

int main() {
  int wholeNumber{ 42 };

  std::cout << "wholeNumber: " << wholeNumber << "\n";
  std::cout << "&wholeNumber: " << &wholeNumber << "\n";
  std::cout << "*(&wholeNumber): " << *(&wholeNumber) << "\n";
}

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.

#include <iostream>
#include <string>

int main() {
  std::string sillyString{ "The rain in Spain falls mainly due to pain." };
  std::string* stringPointer{ &sillyString }; // Pointer to sillyString

  std::cout << " stringPointer: " << stringPointer << "\n";
  std::cout << "*stringPointer: " << *stringPointer << "\n\n";

  // Change what stringPointer points to:
  std::string secondString{ "WOWZA COWZA!" };
  stringPointer = &secondString;
  std::cout << "After changing what stringPointer points to:\n";
  std::cout << " stringPointer: " << stringPointer << "\n";
  std::cout << "*stringPointer: " << *stringPointer << "\n";
}

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.

#include <iostream>

int main() {
  int primes[4]{ 2, 3, 5, 7 };
  int arrayLength{ std::size(primes) };

  // The primes variable is a pointer to the first element in the array: 
  std::cout << "First element: " << *primes << "\n";

  // Other pointers can also point to the first element:
  int* arrayPointer{ primes };
  std::cout << "First element: " << *arrayPointer << "\n";

  // We can even use a pointer to traverse the array like an iterator:
  for(int* pointer{ primes }; pointer < (primes + arrayLength); ++pointer) {
    std::cout << *pointer << "\n";
  }
}

⚡ 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 than 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 with pointers, there is no such thing as an uninitialized reference or a 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: