Chapter 17: Pointers

Introduction to Pointers

Pointers are objects that hold a memory address (typically of another variable) as their value.

Basic Example

int x{ 5 };
std::cout << x << '\n';  // print the value of variable x
std::cout << &x << '\n'; // print the memory address of variable x, hexadecimal values

std::cout << *(&x) << '\n'; // print the value at the memory address of variable x (parentheses not required, but make it easier to read)
int x{ 5 };
int* ptr { &x }; // a pointer initialized with the address of variable x
std::cout << *ptr << '\n'; // use dereference operator to print the value at the address that ptr is holding

*ptr = 6; // The object at the address held by ptr (x) assigned value 6
std::cout << x << '\n'; // 6

Multiple Pointers Declaration

int* ptr1, ptr2;   // incorrect: ptr1 is a pointer to an int, but ptr2 is just a plain int!
int* ptr3, * ptr4; // correct: ptr3 and ptr4 are both pointers to an int

Pointer Initialization and Usage

int x{ 5 };
int y{ 6 };
int* ptr { &x }; // ptr points to x
ptr = &y; // change ptr to point at y
std::cout << *ptr << '\n'; // use dereference operator to print the value at the address that ptr is holding (which is y's address)

Pointer vs Reference

Type of Pointers

int x{ 4 };
std::cout << typeid(&x).name() << '\n'; // print the type of &x : int *

Null and Dangling Pointers

#include <cstddef> // for NULL

float* ptr2; // ptr2 is uninitialized
double* ptr1 { NULL }; // ptr1 is a null pointer
int* ptr3 { nullptr }; // ptr3 is a null pointer

if (ptr3 == nullptr) // explicit test for equivalence
    std::cout << "ptr3 is null\n";

if (ptr3) // if ptr3 is not a null pointer
    std::cout << *ptr3 << '\n'; // okay to dereference

Const Pointers

const int x { 5 };
const int* ptr { &x }; // can't change value but can change what's pointing at
// *ptr = 6; // not allowed: we can't change a const value
ptr = &y;

Const Pointer to Non-const Value

int x{ 5 };
int* const ptr { &x }; // const after the asterisk means this is a const pointer
// ptr = &y; // error: once initialized, a const pointer cannot be changed.
*ptr = 6; // okay: the value being pointed to is non-const

Const Pointer to Const Value

const int* const ptr { &value }; // a const pointer to a const value

Passing by Value, Reference, and Address

void printByValue(std::string val) // The function parameter is a copy of str
{
    std::cout << val << '\n'; // print the value via the copy
}

void printByReference(const std::string& ref) // The function parameter is a reference that binds to str
{
    std::cout << ref << '\n'; // print the value via the reference
}

void printByAddress(const std::string* ptr) // The function parameter is a pointer that holds the address of str
{
    std::cout << *ptr << '\n'; // print the value via the dereferenced pointer
}

std::string str{ "Hello, world!" };

printByValue(str);      // pass str by value, makes a copy of str
printByReference(str);  // pass str by reference, does not make a copy of str
printByAddress(&str);   // pass str by address, does not make a copy of str

Ensuring Valid Pointer

void print(int* ptr)
{
    assert(ptr); // fail the program in debug mode if a null pointer is passed (since this should never happen)

    if (!ptr) // if ptr is a null pointer, early return back to the caller
        return;
    // if we reached this point, we can assume ptr is valid
}

Optional Parameters

#include <iostream>
#include <optional> // for std::optional (C++17)

void printIDNumber(const int* id = nullptr)
{
    if (id)
        std::cout << "Your ID number is " << *id << ".\n";
    else
        std::cout << "Your ID number is not known.\n";
}

void printIDNumber(std::optional<const int> id = std::nullopt)
{
    if (id)
        std::cout << "Your ID number is " << *id << ".\n";
    else
        std::cout << "Your ID number is not known.\n";
}

Using std::optional

#include <iostream>
#include <optional> // for std::optional (C++17)

// Our function now optionally returns an int value
std::optional<int> doIntDivision(int x, int y)
{
    if (y == 0)
        return {}; // or return std::nullopt
    return x / y;
}

int main()
{
    std::optional<int> result1 { doIntDivision(20, 5) };
    if (result1) // if the function returned a value
        std::cout << "Result 1: " << *result1 << '\n'; // get the value
    else
        std::cout << "Result 1: failed\n";
    return 0;
}

Working with std::optional

std::optional<int> o1 { 5 };            // initialize with a value
std::optional<int> o2 {};               // initialize with no value
std::optional<int> o3 { std::nullopt }; // initialize with no value

if (o1.has_value()) // call has_value() to check if o1 has a value
if (o2)             // use implicit conversion to bool to check if o2 has a value

std::cout << *o1;             // dereference to get value stored in o1 (undefined behavior if o1 does not have a value)
std::cout << o2.value();      // call value() to get value stored in o2 (throws std::bad_optional_access exception if o2 does not have a value)
std::cout << o3.value_or(42); // call value_or() to get value stored in o3 (or value `42` if o3 doesn't have a value)

Using the Arrow Operator

Employee* ptr{ &joe };
std::cout << ptr->id << '\n'; // Better: use -> to select member from pointer to object

// access via operator.
std::cout << (*(*ptr).c).y << '\n'; // ugly!

// access via operator->
std::cout << ptr -> c -> y << '\n'; // much nicer

std::cout << (ptr->paw).claws << '\n';