Null Pointers

In the previous lesson, we covered pointer basics: objects holding addresses of other objects. This address can be dereferenced using the dereference operator (*) to get the object at that address:

#include <iostream>

int main()
{
    int health{100};
    std::cout << health << '\n'; // Print health's value

    int* ptr{&health}; // ptr holds health's address
    std::cout << *ptr << '\n'; // Use dereference to print value at ptr's address

    return 0;
}

This example prints:

100
100

We also noted that pointers don't need to point to anything. In this lesson, we'll explore such pointers further.

Null Pointers

Besides a memory address, there's one additional value a pointer can hold: a null value. A null value (often shortened to null) is a special value meaning something has no value. When a pointer holds a null value, it means the pointer isn't pointing at anything. Such a pointer is called a null pointer.

The easiest way to create a null pointer is using value initialization:

int main()
{
    int* ptr{}; // ptr is now a null pointer, not holding an address

    return 0;
}

Best Practice

Value initialize your pointers (to be null pointers) if you're not initializing them with a valid object's address.

Because we can use assignment to change what a pointer points to, a pointer initially set to null can later point at a valid object:

#include <iostream>

int main()
{
    int* ptr{}; // ptr is a null pointer

    int health{100};
    ptr = &health; // ptr now pointing at health (no longer null)

    std::cout << *ptr << '\n'; // Print health's value through dereferenced ptr

    return 0;
}

The nullptr Keyword

Just as keywords true and false represent Boolean literal values, the nullptr keyword represents a null pointer literal. We can use nullptr to explicitly initialize or assign a pointer a null value:

int main()
{
    int* ptr{nullptr}; // Can use nullptr to initialize a pointer to null

    int score{100};
    int* ptr2{&score}; // ptr2 is a valid pointer
    ptr2 = nullptr; // Can assign nullptr to make the pointer null

    someFunction(nullptr); // Can pass nullptr to a function with a pointer parameter

    return 0;
}

Best Practice

Use nullptr when you need a null pointer literal for initialization, assignment, or passing a null pointer to a function.

Dereferencing a Null Pointer Results in Undefined Behavior

Just like dereferencing a dangling (or wild) pointer leads to undefined behavior, dereferencing a null pointer also leads to undefined behavior. In most cases, it will crash your application.

This program illustrates this, and will probably crash when you run it:

#include <iostream>

int main()
{
    int* ptr{}; // Create a null pointer
    std::cout << *ptr << '\n'; // Dereference the null pointer

    return 0;
}

Conceptually, this makes sense. Dereferencing a pointer means "go to the address the pointer is pointing at and access the value there". A null pointer holds a null value, which semantically means the pointer isn't pointing at anything. So what value would it access?

Accidentally dereferencing null and dangling pointers is one of the most common mistakes C++ programmers make, and is probably the most common reason C++ programs crash in practice.

Warning

Whenever you're using pointers, be extra careful that your code isn't dereferencing null or dangling pointers, as this will cause undefined behavior (probably a crash).

Checking for Null Pointers

Just as we can use a conditional to test Boolean values for true or false, we can use a conditional to test whether a pointer has value nullptr:

#include <iostream>

int main()
{
    int health{100};
    int* ptr{&health};

    if (ptr == nullptr) // Explicit test for equivalence
        std::cout << "ptr is null\n";
    else
        std::cout << "ptr is non-null\n";

    int* nullPtr{};
    std::cout << "nullPtr is " << (nullPtr==nullptr ? "null\n" : "non-null\n");

    return 0;
}

This program prints:

ptr is non-null
nullPtr is null

Pointers implicitly convert to Boolean values: a null pointer converts to false, and a non-null pointer converts to true. This allows us to skip explicitly testing for nullptr:

#include <iostream>

int main()
{
    int health{100};
    int* ptr{&health};

    // Pointers convert to Boolean false if null, and Boolean true if non-null
    if (ptr) // Implicit conversion to Boolean
        std::cout << "ptr is non-null\n";
    else
        std::cout << "ptr is null\n";

    int* nullPtr{};
    std::cout << "nullPtr is " << (nullPtr ? "non-null\n" : "null\n");

    return 0;
}

Warning

Conditionals can only differentiate null pointers from non-null pointers. There's no convenient way to determine whether a non-null pointer is pointing to a valid object or dangling.

Use nullptr to Avoid Dangling Pointers

We need to ensure our code doesn't dereference null or dangling pointers.

We can avoid dereferencing a null pointer by using a conditional to ensure a pointer is non-null first:

// Assume ptr is some pointer that may or may not be null
if (ptr) // If ptr is not null
    std::cout << *ptr << '\n'; // Valid to dereference
else
    // Do something else that doesn't involve dereferencing ptr

But what about dangling pointers? Because there's no way to detect whether a pointer is dangling, we need to avoid having any dangling pointers in the first place. We do that by ensuring any pointer not pointing at a valid object is set to nullptr.

That way, before dereferencing a pointer, we only need to test whether it's null. If it's non-null, we assume the pointer isn't dangling.

Best Practice

A pointer should either hold a valid object's address, or be set to nullptr. That way we only need to test pointers for null, and can assume any non-null pointer is valid.

Unfortunately, avoiding dangling pointers isn't always easy: when an object is destroyed, any pointers to that object will be left dangling. Such pointers are not nulled automatically! It's the programmer's responsibility to ensure those pointers are set to nullptr.

Warning

When an object is destroyed, any pointers to the destroyed object will be left dangling (they won't be automatically set to nullptr). It's your responsibility to detect these cases and ensure those pointers are subsequently set to nullptr.

Legacy Null Pointer Literals: 0 and NULL

In older code, you may see two other literal values used instead of nullptr.

The first is the literal 0. In the context of a pointer, literal 0 is specially defined to mean a null value:

int main()
{
    float* ptr{0};  // ptr is now a null pointer (don't do this)

    float* ptr2;
    ptr2 = 0; // ptr2 is now a null pointer (don't do this)

    return 0;
}

As an Aside

On modern architectures, address 0 is typically used to represent a null pointer. However, this value isn't guaranteed by the C++ standard.

Additionally, there's a preprocessor macro named NULL (defined in the header). This macro is inherited from C:

#include <cstddef> // for NULL

int main()
{
    double* ptr{NULL}; // ptr is a null pointer

    double* ptr2;
    ptr2 = NULL; // ptr2 is now a null pointer

    return 0;
}

Both 0 and NULL should be avoided in modern C++ (use nullptr instead). We discuss why in the lesson on pass by address (part 2).

Favor References Over Pointers Whenever Possible

Pointers and references both give us the ability to access some other object indirectly.

Pointers have the additional abilities of being able to change what they're pointing at, and to be pointed at null. However, these pointer abilities are also inherently dangerous: A null pointer runs the risk of being dereferenced, and the ability to change what a pointer is pointing at can make creating dangling pointers easier:

int main()
{
    int* ptr{};

    {
        int health{100};
        ptr = &health; // Assign the pointer to an object that will be destroyed
    } // ptr is now dangling and pointing to invalid object

    if (ptr) // Condition evaluates to true because ptr is not nullptr
        std::cout << *ptr; // Undefined behavior

    return 0;
}

Since references can't be bound to null, we don't have to worry about null references. And because references must be bound to a valid object upon creation and then can't be reseated, dangling references are harder to create.

Because they're safer, references should be favored over pointers, unless the additional capabilities provided by pointers are required.

Best Practice

Favor references over pointers unless the additional capabilities provided by pointers are needed.

Quiz

Question 1

1a) Can we determine whether a pointer is null or not? If so, how?

Answer: Yes, we can use a conditional (if statement or conditional operator) on the pointer. A pointer will convert to Boolean false if it's null, and true otherwise.

1b) Can we determine whether a non-null pointer is valid or dangling? If so, how?

Answer: There's no easy way to determine this.

Question 2

For each subitem, answer whether the action described will result in behavior that is: predictable, undefined, or possibly undefined. If the answer is "possibly undefined", clarify when.

2a) Assigning an object's address to a non-const pointer

Answer: Predictable. This just copies the address into the pointer object.

2b) Assigning nullptr to a pointer

Answer: Predictable.

2c) Dereferencing a pointer to a valid object

Answer: Predictable.

2d) Dereferencing a dangling pointer

Answer: Undefined.

2e) Dereferencing a null pointer

Answer: Undefined.

2f) Dereferencing a non-null pointer

Answer: Possibly undefined, if the pointer is dangling.

Question 3

Why should we set pointers that aren't pointing to a valid object to nullptr?

Answer: We can't determine whether a non-null pointer is valid or dangling, and accessing a dangling pointer causes undefined behavior. Therefore, we need to ensure we don't have any dangling pointers in our program.

If we ensure all pointers are either pointing to valid objects or set to nullptr, then we can use a conditional to test for null to ensure we don't dereference a null pointer, and assume all non-null pointers are pointing to valid objects.