Skip to content
VLSI Mentor

C++ Part I · Module 8 · Pointers and References

nullptr and Checking Before You Dereference

Chapter 8.2 ended on an admission: every dereference carries a precondition that nothing in the statement reveals, and there are two quite different ways it can be unmet. One is that the pointer holds an address which was valid and no longer is, which is genuinely hard and waits for the end of this module. The other is that the pointer is deliberately holding nothing at all, and that one is tractable — it is a specific, testable value, and a program can ask about it before dereferencing. This chapter is about that value. It needs a name, C++ has accumulated three different ways to write it, and only the newest is reliable. That is not a matter of style: with two overloaded functions, the three spellings produce three different outcomes, one of which is a silent call to the wrong function and one of which refuses to compile. The chapter then takes the frozen curriculum's instruction about checking literally, because it is narrower than it first appears. The instruction is to check every dereference that can receive a null — not every pointer everywhere, which is a habit that trades one bug class for a great deal of noise and a false sense of safety.

Foundation16 min readC++Part IFundamentalsPointers

Part I · Module 8 · Chapter 8.3 · nullptr and Checking Before You Dereference

1. Two Failure Modes, and This Chapter Takes the Easy One

Chapter 8.2 §6's callout established that *p = 7; is correct or undefined depending on a fact not visible in it. There are two ways the fact can be wrong:

FailureMeansTestable?Chapter
nullp deliberately holds no addressyes — it is a specific value8.3, this chapter
danglingp holds an address whose object is goneno8.8

The distinction matters more than it looks, and it is worth fixing now so this chapter is not mistaken for a complete answer.

A null check is possible because null is one known value. You can compare against it.

A dangling check is not possible. A pointer to a destroyed object holds a perfectly ordinary-looking address. There is no value to compare against, because the bits are indistinguishable from a valid pointer — Chapter 7.2 §4 measured storage being reused, which is exactly why.

So: checking for null is necessary and nowhere near sufficient. A codebase with a null check before every dereference can still be riddled with Chapter 8.8's bug, and a reader who has only this chapter will believe otherwise.

2. The Deliberate Nothing

A pointer sometimes needs to mean "not pointing at anything" — a search that found no match, an optional parameter that was not supplied, the end of a chain. That requires a value distinguishable from every valid address, and C++ guarantees one exists.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int* p = nullptr;        // deliberately points at nothing

Two properties are guaranteed:

It compares unequal to the address of any object. So p == nullptr is a reliable test.

It converts to false in a condition. So if (p) means "if p points at something".

Both measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a null int* compares equal to nullptr : true
a null int* is false in a condition   : false

Dereferencing it is undefined behaviour. Not "returns zero", not "crashes" — undefined, in Chapter 1.3 §5's sense.

What happened here, as an observation and not a rule:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic nullderef.cpp -o nullderef
$                                            ← zero warnings
$ ./nullderef
before
$ echo $?
139

Exit 139 is 128 + 11, which is SIGSEGV. before printed because std::endl flushed it; after was never reached. Chapter 5.4 §5's discipline applies — that is what one build on one machine did, and the standard promises none of it. A crash is the common outcome and it is why §8 calls this the friendly failure, but §6 is about checking rather than about catching.

3. What nullptr Actually Is

nullptr is a keyword, added in C++11, and it has a type:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
decltype(nullptr) is std::nullptr_t : true
sizeof(std::nullptr_t) = 8

std::nullptr_t is a distinct type whose only value is nullptr. It converts to any pointer type and to no integer type, and that one-way conversion is the whole reason it fixes §4's problem.

It is not a macro, not an integer, and not 0 with a different name.

4. Why It Replaced 0 and NULL

Here is the measurement the frozen purpose asks for. Two overloads — one taking an int, one taking a pointer — and the three spellings of "nothing":

Azvya Education Pvt. Ltd.VLSI Mentor
nullover.cpp — complete program
#include <cstddef>
#include <iostream>

static void f(int v)         { std::cout << "  f(int) got "   << v << '\n'; }
static void f(const char* v) { std::cout << "  f(const char*) got " << (v ? v : "(null)") << '\n'; }

int main()
{
    std::cout << "f(0):\n";       f(0);
    std::cout << "f(nullptr):\n"; f(nullptr);
    return 0;
}

Measured output, zero warnings:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
f(0):
  f(int) got 0
f(nullptr):
  f(const char*) got (null)

f(0) called f(int). The literal 0 is an int, so Chapter 6.7 §3's rank order gives the int overload an exact match, which beats any conversion. The author who wrote f(0) meaning "no string" silently got the integer function. No diagnostic.

f(nullptr) called f(const char*), because std::nullptr_t converts to a pointer type and not to int. There is only one viable candidate.

And the third spelling, in the same program:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nullmacro.cpp:5:12: error: call to 'f' is ambiguous
nullmacro.cpp:3:13: note: candidate function
nullmacro.cpp:4:13: note: candidate function

f(NULL) does not compile.

SpellingOutcomeWhy
0silently calls f(int)0 is an int; exact match wins
NULLambiguous — does not compilesee below
nullptrcalls f(const char*)converts to pointers only

About the NULL result. NULL is a macro, and what it expands to is implementation-defined. On this implementation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -E -P nullexp.cpp
NULL_IS __null

__null is a compiler extension that converts to pointers and to integers, which is why neither candidate is better and the call is ambiguous. A different implementation may define NULL as 0, in which case the call would behave like the first row and silently pick f(int).

That variability is itself the argument. The lesson is not "NULL is ambiguous" — it is that NULL's behaviour depends on your standard library, so it is not something you can reason about portably. nullptr has one meaning everywhere.

5. The Second Reason: It Says What It Is

The overload case is the measurable argument. There is a second one that matters more in ordinary code.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int* p = 0;              // is this a pointer, or a number that happens to be zero?
    int* q = nullptr;        // unambiguous

0 is an integer literal that C++ permits as a null pointer constant for historical reasons. nullptr is a value that can only be a pointer. A reader of the second line does not have to decide which is meant, and neither does a search tool.

It also reads correctly in a comparison:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if (p == nullptr)        // "p points at nothing"
    if (p == 0)              // "p equals zero" -- true but misleading

6. Checking Before Dereferencing

The precondition from Chapter 8.2 §3 is that p holds the address of a live object. When p can be null, the check is a conditional:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if (p != nullptr) {
        *p = 7;
    }

or, equivalently and more commonly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if (p) {
        *p = 7;
    }

Both are fine. if (p) relies on §2's guaranteed conversion to false, and it is the idiomatic form; if (p != nullptr) is more explicit and a reasonable house style. Chapter 5.1 §3 made the general argument for saying what is being tested, and here both forms say it.

The early return form is usually clearer than nesting, and Chapter 6.3 §9's discipline applies — a function that cannot proceed should say so at the top:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static void scale(int* value, int factor)
    {
        if (value == nullptr) return;      // nothing to do
        *value *= factor;
    }

Measured: scale(&n, 3) on n = 6 gives 18, and scale(nullptr, 3) returns without incident.

7. Where the Check Belongs — and Where It Does Not

The frozen purpose says to check every dereference that can receive one. That is narrower than "check every pointer", and the difference is the whole of this section.

Check when null is a legitimate input. If the function's contract says the pointer may be null — an optional parameter, a search result, the end of a list — then handling null is part of implementing the contract, not a defensive extra.

Do not check when null is not a legitimate input. If a function requires a valid pointer, a null is a bug in the caller. Adding if (p) ... else return; does not fix that bug; it hides it. The function now silently does nothing, the caller believes it worked, and the actual defect surfaces later and elsewhere.

That second case has two honest responses:

Take a reference instead. Chapter 8.5 establishes that a reference cannot be null, so a parameter that must be present should be a reference and the question disappears at compile time. Chapter 8.6 is the chapter that makes this decision properly, and this is its single strongest argument.

Or assert. When a pointer is genuinely required but a reference will not do, assert(p != nullptr); documents the requirement and fails loudly in a debug build rather than quietly in production. Chapter 25.5's territory covers this properly; the point here is that a check which returns silently and a check which fails loudly are different things, and only one of them is appropriate for a broken precondition.

A null check at a boundary where null is meaningful is correct. A null check that converts a caller's bug into a silent no-op is a second bug.

8. The Design Question Behind It

A pointer parameter that may be null is doing two jobs: carrying a value, and carrying the possibility of absence. Chapter 8.6's measurement makes the contrast concrete — a pointer parameter can express "nothing supplied" and a reference parameter cannot:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
maybe_report(&n):      value = 7
maybe_report(nullptr): no value supplied
must_report(n):        value = 7

So the choice of parameter type is the choice of whether absence is representable, and that is a decision about the interface rather than about convenience. Chapter 8.6 is built on it.

One forward note, because it changes the answer in modern code. When you need "a value that may be absent" and do not otherwise need a pointer, C++17 provides a type that says exactly that and cannot be dereferenced without being checked — Chapter 21.5's std::optional. A nullable pointer used purely as an optional value is the older idiom, and the newer one makes the absence part of the type rather than part of the documentation.

9. What to Actually Do

Write nullptr. Not 0, not NULL, in new code and in code you are editing.

Initialize a pointer that does not yet point at anything to nullptr rather than leaving it uninitialized — Chapter 3.8's rule, and an uninitialized pointer is worse than a null one because it cannot be tested.

Check where null is a legitimate input, and handle it as part of the contract.

Do not check where null is a caller's bug. Take a reference (Chapter 8.6) or assert.

Prefer an early return to a nested if when the null case means "nothing to do".

Use if (p) or if (p != nullptr) — both are fine; be consistent.

Remember a null check is not a validity check. Chapter 8.8 is the rest of it.

Reach for std::optional when you want absence without needing a pointer — Chapter 21.5.

10. Mistakes Worth Making Once

Writing f(0) to mean a null pointer. Measured: it called f(int) silently.

Writing f(NULL) and expecting portable behaviour. Measured: ambiguous here; NULL's expansion is implementation-defined.

Thinking nullptr is just a tidier 0. It has a distinct type that converts to pointers and not to integers.

Believing a null check makes a dereference safe. It rules out one of the two failure modes.

Wrapping a required pointer in if (p). It converts a located crash into a silent wrong answer.

Leaving a pointer uninitialized instead of null. An indeterminate pointer cannot be tested — Chapter 3.8.

Expecting a null dereference to "return zero". It is undefined behaviour; a crash is the usual outcome, not the specified one.

Taking an absence of null crashes as evidence of pointer safety. It says nothing about lifetime bugs.

11. Interview Relevance

"Why nullptr rather than NULL or 0?" The complete answer is the overload case, and the measurement is what makes it convincing: with f(int) and f(const char*), f(0) silently calls the integer overload, f(NULL) is implementation-dependent and was ambiguous here, and f(nullptr) calls the pointer one. Then the type reason: std::nullptr_t converts to pointer types and not to integers.

"Should you check every pointer for null?" No, and the reasoning is the discriminator. Check where null is a legitimate input; where it is not, a null is a caller's bug, and silently returning hides it. Prefer a reference when the argument must be present.

"What happens when you dereference a null pointer?" Undefined behaviour. A crash is the usual outcome on a hosted platform because address zero is unmapped, and that is an observation rather than a guarantee.

12. What You Should Be Able to Say Now

  1. Two failure modes: null (a specific, testable value) and dangling (an ordinary-looking address). This chapter is the first only.
  2. A dangling pointer cannot be tested for — Chapter 7.2 §4's storage reuse is why.
  3. nullptr compares unequal to every object's address and converts to false — measured both.
  4. Dereferencing it is undefined behaviour, not a specified crash.
  5. decltype(nullptr) is std::nullptr_t — a distinct type whose only value is nullptr, converting to pointers and not to integers.
  6. Measured: f(0) called f(int) silently; f(nullptr) called f(const char*).
  7. Measured: f(NULL) was error: call to 'f' is ambiguous, because NULL expands to __null here.
  8. NULL's expansion is implementation-defined, so its behaviour is not portably reasonable about — which is the real argument.
  9. nullptr also says what it is at a declaration and in a comparison.
  10. Check where null is a legitimate input; handle it as part of the contract.
  11. Do not check where null is a caller's bug — that converts a located crash into a silent wrong answer.
  12. The two honest alternatives: take a reference (it cannot be null) or assert.
  13. A null dereference is the friendly failure — immediate and at the cause, unlike Chapter 8.8's.
  14. Measured: a pointer parameter can express "nothing supplied"; a reference cannot — which is Chapter 8.6's decision, and Chapter 21.5's std::optional is the modern form when no pointer is otherwise needed.

13. Exercises

Exercise 1 — predict the call. With void g(int); and void g(double*); overloaded, say which is called and what happens: (a) g(0) (b) g(nullptr) (c) g(NULL) (d) g(1).

Exercise 2 — name the type. Give the type of nullptr, say what values it has, and name the two conversions it does and does not permit.

Exercise 3 — which failure? For each, say whether a null check would help, and name the chapter that covers it: (a) a pointer parameter the caller left unset (b) a pointer to a local that has gone out of scope (c) a search function that found no match (d) a pointer into a container that was resized.

Exercise 4 — fix the signature. void scale(int* value, int factor) requires a valid pointer and currently returns silently when given null. Give two different repairs and say which you would ship.

Exercise 5 — read the ambiguity. f(NULL) failed here and may compile elsewhere. Explain why, give the expansion that caused it, and state what you would conclude about using NULL at all.

Exercise 6 — the quiet bug. A reviewer adds if (p) around a dereference in a function whose contract requires a valid pointer, and the crash reports stop. Say what has actually changed and what has not.

Exercise 7 — which mechanism? For each, choose a nullable pointer, a reference, or std::optional, and justify: (a) a function that must modify its argument (b) a lookup that may find nothing (c) an optional output the caller may not want (d) a parameter that is always required (e) the end of a linked chain.

14. Summary

  • Two failure modes: null, which is testable, and dangling, which is not. This chapter is the first.
  • nullptr compares unequal to every address and converts to false; dereferencing it is undefined.
  • std::nullptr_t converts to pointer types and not to integers.
  • Measured: f(0) → f(int) silently; f(NULL) → ambiguous; f(nullptr) → f(const char*).
  • NULL expands to __null here and is implementation-defined in general — that is the real argument.
  • Check where null is a legitimate input; where it is a caller's bug, take a reference or assert.
  • A silent check is a second bug — Chapter 6.3 §9's discipline, restated.
  • A null dereference is the friendly failure: immediate, and at the cause.
  • A null check is not a validity check — Chapter 8.8 is the rest.
  • std::optional is the modern form of "may be absent" when no pointer is needed.

A pointer holding one address is the simple case, and this chapter's testable failure is the simple failure. The next chapter is about pointers that hold one address and then another, and it answers a question the definition in Chapter 8.1 leaves open: if a pointer's value is just a number, what does adding one to it mean? The answer is not "the next byte", it is different for every pointer type even though every pointer is the same size, and the measurement makes Chapter 8.1 §5's claim about what the type adds concrete in a way nothing else does — three pointer types, all eight bytes wide, stepping four, eight and one byte respectively.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.