NULL vs nullptr

Hi everyone ✋

In the previous post we learned how to lock our pointers down with const ─ freezing either the value a pointer looks at, the address it holds, or both. We spent that whole post deciding what a pointer is allowed to change.

Today we ask a different question ─ what if a pointer should point at nothing at all? 🤔

If we haven’t read the earlier posts in this series, it’s worth going through them first ─ especially the intro post, because today builds directly on the idea that a pointer is just a variable holding an address.

Today’s topic is NULL vs nullptr ─ the two ways C++ lets us say “this pointer leads nowhere.” They look almost interchangeable, and for years people used them as if they were. But there’s a subtle, sneaky difference between them that can quietly send our program down the wrong path 😱. By the end of this post we’ll know exactly why modern C++ invented nullptr, and why we should reach for it every single time.

Let’s take a deep dive 🦴

First ─ what is a null pointer, really? 🤔

We know a pointer holds the address of something. But sometimes we have a pointer that isn’t ready to point at anything yet ─ maybe we haven’t decided what it should point at, or maybe it used to point somewhere and that thing is now gone.

For exactly these moments, C++ gives us the idea of a null pointer ─ a pointer that deliberately points at nothing, and says so honestly.

#include <iostream>
using namespace std;

int main()
{
    int *ptr = nullptr;   // a pointer that deliberately points at nothing

    if (ptr == nullptr)
        cout << "ptr currently points at nothing" << endl;

    return 0;
}

The output will be ─

ptr currently points at nothing

This is a hugely important habit ⚠️. Remember from the earlier posts that a pointer we never initialized is a wild pointer ─ it holds some random leftover address, and using it is a recipe for disaster. A null pointer is the opposite ─ it’s a known, safe, intentional “empty.” We can always test for it with if (ptr == nullptr) before we dare to use it. Setting a pointer to null is how we say “this leads nowhere ─ don’t follow it yet.”

So the concept is simple. The interesting part is how we write that “nothing” ─ and that’s where our two contenders come in.

The old guard ─ 0 and NULL

Back in the early days of C and C++, there was no special word for a null pointer. Programmers just used plain old 0

int *ptr = 0;   // the old-school way to say "points at nothing"

This works because the language has a special rule ─ the literal 0 can be turned into a null pointer of any type. So int *ptr = 0; is perfectly legal and means “null.”

Later, to make the intent clearer, the macro NULL appeared. But here’s the twist that this whole post hinges on 👇 ─ in C++, NULL is usually just 0 wearing a costume. On many systems it’s literally defined as ─

#define NULL 0

So when we write NULL, the compiler very often sees nothing more than the integer 0. It looks like it belongs to the world of pointers, but underneath, it’s an integer. And that innocent little detail is the source of all our trouble 😅

Let’s give it a picture before we see the trap.

My little reference ─ the mailroom and the address slip 📬

Here’s the picture to keep in mind for the whole post.

Think of a pointer as a slip of paper with a house address written on it. Following the pointer is like a courier driving to that address. A null pointer is a slip that deliberately reads “no address ─ nobody home.” It’s a valid, honest slip ─ very different from a blank one covered in random scribbles (that would be our wild pointer from the earlier posts).

Now, how do we write “nobody home” on the slip? That’s where our two options differ ─

  • The old way (0 / NULL) scrawls the digit 0 in the address box. The problem ─ to anyone who only reads numbers, “0” looks like it could be house number 0. Is it “no address,” or is it a real address that happens to be zero? The slip is ambiguous.
  • nullptr uses a dedicated “NO ADDRESS” stamp ─ a special mark that can never be mistaken for a house number. Everyone who reads it knows instantly: this slip is about addresses, and this one leads nowhere.

Now picture the mailroom with two clerks ─ a numbers clerk who deals with plain house numbers, and a delivery clerk who handles address slips. Hand them a slip marked “0” and the numbers clerk snatches it up (after all, 0 is a number!). Hand them a slip stamped “NO ADDRESS,” and it goes straight to the delivery clerk, exactly as we intended.

Keep this mailroom in mind ─ it’s about to explain a very real bug 😉

The trap ─ when the “0” slip goes to the wrong clerk 😱

Here’s where NULL bites us. Suppose we have two overloaded functions ─ same name, but one takes an int (our numbers clerk) and the other takes a pointer (our delivery clerk).

#include <iostream>
using namespace std;

void printValue(int n)    { cout << "called printValue(int): " << n << endl; }
void printValue(int *p)   { cout << "called printValue(int*)"       << endl; }

int main()
{
    printValue(0);         // which clerk takes it?
    printValue(nullptr);   // which clerk takes it?

    return 0;
}

The output will be ─

called printValue(int): 0
called printValue(int*)

Look closely at that first line 😱. We wrote printValue(0) ─ and even though 0 can mean a null pointer, the compiler saw a plain integer and handed it to the int version. If our intent was to pass a null pointer, the wrong function just ran, silently, with no warning. The “0” slip went to the numbers clerk.

Now the second line ─ printValue(nullptr) went cleanly to the pointer version, exactly as we meant. The “NO ADDRESS” stamp reached the delivery clerk 🥳

“But surely NULL is smarter than a bare 0?” ─ you might hope. Let’s try it ─

printValue(NULL);   // 🤔 what happens here?

On our compiler (GCC), this doesn’t even build ─ it complains ─

error: call of overloaded 'printValue(NULL)' is ambiguous

Because NULL is really that integer-flavored 0, the compiler genuinely can’t decide whether we meant the number or the pointer ─ our two clerks stand there arguing over the slip. On other compilers where NULL is plainly 0, it doesn’t argue at all ─ it quietly calls the int version, just like printValue(0) did. Either way the outcome is wrong for something we intended as a pointer. NULL simply can’t be trusted to behave like a pointer, because deep down it isn’t one 😅

Enter nullptr ─ a real null at last 🥳

C++11 fixed this properly by introducing a brand-new keyword ─ nullptr. Unlike NULL, it is not an integer in disguise. It has its very own type, std::nullptr_t, and its entire job in life is to represent a null pointer ─ nothing else.

That’s the “NO ADDRESS” stamp from our mailroom, made real. Because nullptr has a pointer-flavored type of its own ─

  • It converts happily to any pointer type (int*, char*, double*, a pointer to our own class ─ anything).
  • It refuses to convert to an int. It will never wander into the numbers clerk’s line by mistake.

That single property is what made printValue(nullptr) reach the pointer overload cleanly in the example above. No ambiguity, no silent misfire ─ the right function, every time.

Let’s confirm what nullptr will and won’t allow ─

#include <iostream>
using namespace std;

int main()
{
    int    *p = nullptr;   // ✅ any pointer type accepts it
    double *q = nullptr;   // ✅
    char   *r = nullptr;   // ✅

    // int n = nullptr;    // ❌ ERROR ─ nullptr is NOT an integer

    if (!p)
        cout << "p is null" << endl;   // ✅ still works in a boolean test

    return 0;
}

The output will be ─

p is null

If we uncomment that int n = nullptr; line, the compiler stops us at once ─

error: cannot convert 'std::nullptr_t' to 'int' in initialization

And that error is a gift 🎁. It’s the compiler refusing to let us confuse “no address” with “the number zero” ─ the exact confusion that caused the whole mess with NULL.

Notice, too, that nullptr still plays nicely in a boolean test ─ if (!p) and if (p == nullptr) both work, so our old null-checking habits carry over unchanged. We gained safety without losing any convenience 🥳

nullptr and everything we’ve built 🧩

The lovely thing about nullptr is how naturally it slots into every idea from the earlier posts.

Remember functions that hand back a pointer? A common convention is to return null when there’s nothing to give back ─ “I looked, and found nothing.” With nullptr, that intent reads perfectly ─

#include <iostream>
using namespace std;

// return a pointer to the first even number, or null if there isn't one
int* firstEven(int *arr, int size)
{
    for (int i = 0; i < size; i++)
        if (arr[i] % 2 == 0)
            return &arr[i];

    return nullptr;   // nothing found ─ honestly report "no address"
}

int main()
{
    int a[] = { 1, 3, 5, 7 };

    int *found = firstEven(a, 4);

    if (found == nullptr)
        cout << "no even number in the array" << endl;
    else
        cout << "found: " << *found << endl;

    return 0;
}

The output will be ─

no even number in the array

See how clean that reads? The function promises “a pointer, or nullptr if nothing”, and the caller checks if (found == nullptr) before daring to dereference. That guard is what keeps us away from the dreaded null dereference ─ and it’s the same pattern we lean on with smart pointers, which compare against nullptr too. Once we start using nullptr everywhere, our pointer code simply reads more honestly 👀

One thing nullptr does not fix ⚠️

Let’s be crystal clear about the limits, because this trips people up.

nullptr makes the null value type-safe. It does not make it safe to actually follow a null pointer. Dereferencing a null pointer ─ whether we spelled it 0, NULL, or nullptr ─ is still undefined behaviour, the crash-or-corruption gremlin we keep meeting ☠️.

int *p = nullptr;

// cout << *p;   // ☠️ undefined behaviour ─ we're chasing an address that leads nowhere

In our mailroom, nullptr guarantees the right clerk reads the slip ─ but if the courier actually tries to drive to “NO ADDRESS,” they still crash into a wall. So nullptr is about picking the correct type and overload; checking before we dereference is still entirely our job. The two protections work together, and we need both.

Gotchas to keep us safe ⚠️

Let’s gather every tripwire in one place ─

  • NULL is really an integer. In C++ it’s usually just 0, so it can silently pick an int overload (or trigger an “ambiguous” error) instead of the pointer one. Prefer nullptr and this whole class of bug disappears.
  • 0 as a null pointer is legal but muddy. It works, but it hides intent ─ a reader can’t tell whether we meant a number or a null pointer. nullptr says exactly what we mean.
  • nullptr is not an int. int n = nullptr; won’t compile ─ and that’s a feature, not a nuisance.
  • nullptr still needs C++11 or newer. On a truly ancient compiler it won’t exist. On anything modern, it’s always available ─ just make sure the project isn’t stuck on a pre-2011 standard.
  • nullptr does not make dereferencing safe. Following a null pointer is undefined behaviour no matter how we spelled the null. Before you follow a pointer, guard it with if (ptr) or if (ptr == nullptr).
  • Boolean checks still work. if (ptr) and if (!ptr) behave exactly as before ─ switching to nullptr costs us none of our familiar habits.

Follow these and null pointers become calm, predictable, and honest 🥳

So what should we remember? 🤔

Let’s wrap up the key takeaways ─

  • A null pointer is a pointer that deliberately points at nothing ─ a safe, intentional “empty,” and the opposite of an uninitialized wild pointer.
  • Picture the mailroom 📬 ─ a null is an address slip that reads “no address.” The old 0/NULL scrawls the digit “0” (which looks like a house number); nullptr uses an unmistakable “NO ADDRESS” stamp.
  • NULL is an integer in disguise ─ in C++ it’s usually just 0. With overloaded functions it can silently call the int version, or fail to compile as ambiguous. Either way, it doesn’t reliably behave like a pointer.
  • nullptr (C++11) is a real null with its own type, std::nullptr_t. It converts to any pointer type but refuses to become an int, so it always resolves to the pointer overload we meant.
  • nullptr still works in boolean tests (if (ptr), if (ptr == nullptr)), so none of our null-checking habits change.
  • It reads beautifully with everything we’ve built ─ functions that return a pointer or nullptr, and smart pointers that compare against nullptr.
  • nullptr fixes the type of null, not the danger of following one ─ dereferencing any null pointer is still undefined behaviour ☠️. Guard first, then dereference.
  • The rule of thumb for modern C++ is simple ─ use nullptr, always. Leave 0 and NULL in the history books.

Once this clicks, we stop seeing null as a fuzzy “zero-ish” thing and start seeing it as what it truly is ─ a proper, type-safe way to say “this pointer leads nowhere” 👀✨

Congratulations 🥳🥳🥳
We now understand the real difference between NULL and nullptr ─ why the old ways are quietly unsafe, and why nullptr is the honest, modern choice.

In the next post we’ll look at member access through -> ─ once we hold a pointer to a struct or a class object, how do we reach the members inside it? We’ll meet the arrow operator, and see why ptr->member is really just a friendly shorthand for (*ptr).member 😉

Happy Coding 💻 🎵

Leave a Comment

Your email address will not be published. Required fields are marked *