C Part I · Module 7 · Pointers
Null Pointers
Every pointer in this module so far has been initialised from the address of an object sitting a few lines above it, which made dereferencing it unconditionally safe and made the question of whether it pointed anywhere seem like an odd one to ask. Real code is not like that. A pointer is routinely declared before there is anything for it to identify, handed back by a function that may not have succeeded, or stored somewhere that has only been partly filled in. C provides a single value whose entire job is to say that a pointer deliberately identifies no object, and a rule about what happens if you use one anyway. The rule is short. The discipline that follows from it is the real content of this chapter, because the compiler will not help you — not even in the most obvious case there is.
Foundation16 min readCPart IFundamentalsPointers
Part I · Module 7 · Chapter 7.4 · Null Pointers
1. The Value That Means "Nothing"
A pointer holds an address. Sometimes the honest answer to which object does this identify? is none — not yet, or not any more, or because the operation that was supposed to supply one failed.
C has a value for that, written NULL:
int *p = NULL;NULL comes from several standard headers, including <stddef.h> and <stdio.h>, so the #include <stdio.h> at the top of every program in this curriculum has already been supplying it.
The guarantee is precise and worth stating exactly, because the usual shorthand for it causes trouble later:
A null pointer is guaranteed to compare unequal to a pointer to any object. It is a value that cannot be confused with a real one.
That is the whole promise. Note what it does not say — it says nothing about the number zero.
2. What a Null Pointer Looks Like
#include <stdio.h>
int main(void)
{
int *p = NULL;
printf("p %p\n", (void *)p);
printf("p == NULL %d\n", p == NULL);
printf("p == 0 %d\n", p == 0);
printf("!p %d\n", !p);
if (p) {
printf("truthy\n");
} else {
printf("p is falsy in a condition\n");
}
int n = 5;
p = &n;
printf("after p = &n: p == NULL %d !p %d\n", p == NULL, !p);
return 0;
}Output:
p 0x0
p == NULL 1
p == 0 1
!p 1
p is falsy in a condition
after p = &n: p == NULL 0 !p 0Three things in that output are guarantees and one is not.
p == NULL is 1, and becomes 0 once p identifies an object. Guaranteed.
p == 0 is 1. Guaranteed — and this is the one place a bare 0 means something special. A constant 0 used where a pointer is expected is a null pointer constant: the compiler converts it to a null pointer of the right type. That is why p == 0 works, and why a great deal of existing C writes if (p == 0) rather than if (p == NULL).
!p is 1, and if (p) takes the else branch. Guaranteed, and it follows from rules you already have: Chapter 2.10's truth model says zero is false, and a null pointer compares equal to zero. So if (p) and if (p != NULL) are the same test.
p printed as 0x0. Not guaranteed. This is what this implementation happens to use.
3. What Happens If You Use One
int *p = NULL;
printf("%d\n", *p);Dereferencing a null pointer is undefined behaviour. The C standard imposes no requirements on what the program does.
That is the same classification Chapter 6.4 gave an out-of-range array access, and it is refused in the same way here: this chapter does not run that program and report what it printed, because whatever appeared would be one accident on one machine.
The common description is worth correcting for the same reason Chapter 6.4 corrected "it crashes":
"It segfaults." On a hosted operating system it usually does, because the system deliberately leaves the lowest addresses unmapped so that exactly this mistake is caught. That is common implementation behaviour arising from the operating system, not a rule of C — and on the embedded targets Part II is heading toward, there may be no such protection at all.
4. The Compiler Will Not Catch It
Here is the fact that determines everything else in this chapter.
#include <stdio.h>
int main(void)
{
int *p = NULL;
printf("%d\n", *p);
return 0;
}cc -std=c17 -Wall -Wextra -Wpedantic null.c -o null(no diagnostics)Nothing. The pointer is initialised to NULL two lines above, nothing else touches it, and the dereference is in plain view. If the compiler will not object to the most obvious instance of this mistake that can be constructed, you should not expect it to object to one where the pointer came from a function call thirty lines earlier.
Checking for null is your job, not the compiler's. This is not a warning you forgot to enable.
5. Where to Check — and Where Not To
A blanket rule of check every pointer before every dereference is both wasteful and self-defeating: code full of checks that can never fail trains readers to skim past the ones that can.
The useful rule is narrower:
Check a pointer when it could be null. That means: when it came from outside your function, or from an operation that is allowed to fail.
| Where the pointer came from | Check before dereferencing? |
|---|---|
&local_object a few lines above | no — it cannot be null |
| A parameter, from a caller you do not control | yes |
A function documented as returning NULL on failure | yes, and before anything else |
A pointer you set to NULL yourself to mean "not set yet" | yes |
| A pointer you have already checked in this function, unchanged since | no |
The second row is the one that matters most, and it arrives in the very next chapter. The third becomes routine in Module 9, where malloc is the standard example of an operation that hands back NULL when it cannot do what you asked — Chapter 9.6's subject.
Checking early
The usual shape is a guard at the top of a function, which Chapter 5.4's early-return discussion already recommended:
if (sample_out == NULL) {
return result_bad_argument;
}
/* from here on, sample_out is known good */Two properties make this the right shape. The check happens once, at the boundary where the pointer entered. And after it, the rest of the function can dereference freely without a reader wondering whether it is safe — which is what makes the absence of a check elsewhere informative rather than worrying.
Setting a pointer to NULL deliberately
int *selected = NULL; /* nothing selected yet */
/* ... some logic that may or may not choose something ... */
if (selected != NULL) {
printf("%d\n", *selected);
}This is NULL used as it is meant to be: a value that means something, distinguishable from every real answer. It is the pointer equivalent of Chapter 4.6's search result being initialised to a value that cannot be a real one.
Compare it with leaving the pointer uninitialised. Chapter 2.3 established that an uninitialised object holds an indeterminate value, so an uninitialised pointer does not reliably point at nothing — it points at something unknowable, and if (selected != NULL) would be meaningless. Initialise a pointer to NULL when you do not yet have an object for it.
6. Making It Visible
C gives you no run-time check, but tooling does — the same answer Chapter 6.4 gave for array bounds:
cc -std=c17 -Wall -Wextra -Wpedantic -fsanitize=undefined -g null.c -o nullruntime error: load of null pointer of type 'int'UndefinedBehaviorSanitizer adds the check the language does not require, names the operation, and gives you the line. As in Chapter 6.4, two caveats apply: the report is the tool's, not C's — without it the behaviour is still undefined — and sanitizers are development and test tools rather than something you ship.
Building your tests with -fsanitize=undefined catches null dereferences and out-of-range array accesses with the same flag, which makes it an unusually good return on one line of build configuration.
7. C23: nullptr
C23 adds a keyword for this:
int *p = nullptr;Under this curriculum's C17 baseline it does not exist:
error: use of undeclared identifier 'nullptr'Under -std=c23 it compiles, and p == nullptr is 1.
The motivation is that NULL is permitted to be defined as a plain 0, which is an integer and occasionally participates in overload-like situations and variadic calls in ways that surprise people. nullptr has a pointer type of its own and cannot be mistaken for an integer.
Write NULL on a C17 baseline. Recognise nullptr when you meet it in new code, and know that it means the same thing with fewer sharp edges.
8. What You Should Be Able to Say Now
NULLis the value a pointer holds when it identifies no object.- It is guaranteed to compare unequal to any object's address.
- It is not "address zero" — the bit pattern is unspecified, even though
0x0is what usually prints. - A constant
0used where a pointer is expected is a null pointer constant, which is whyp == 0works. if (p)andif (p != NULL)are the same test, by Chapter 2.10's truth model.- Dereferencing a null pointer is undefined behaviour — no requirements at all.
- "It segfaults" is operating-system behaviour, not a C rule, and may not happen on a bare-metal target.
- The compiler does not warn, even when the pointer is visibly
NULLtwo lines above. - Check where a pointer could be null — from a caller, or from an operation allowed to fail — once, at the boundary.
- Initialise to
NULLwhen you have no object yet; an uninitialised pointer points at something unknowable.nullptris the C23 spelling.
9. Exercises
Exercise 1 — predict the output. Do not compile yet.
int *p = NULL;
int n = 7;
printf("%d %d\n", p == NULL, !p);
p = &n;
printf("%d %d\n", p == NULL, !p);
printf("%d\n", *p);Say which of those five numbers are guaranteed by C and which depend on the implementation.
Exercise 2 — classify four claims. For each, say whether it is accurate, and correct it if not.
NULLis defined to be the integer 0- a null pointer compares unequal to the address of any object
- dereferencing a null pointer always terminates the program
if (p)is a different test fromif (p != NULL)
Exercise 3 — decide where to check. For each pointer, say whether you would check it for null before dereferencing, and why.
int x = 1; int *a = &x; /* a */
void f(int *b) { ... } /* b, inside f */
int *c = NULL; /* ...later maybe assigned */ /* c */
int *d = &x; if (d == NULL) return; ... *d; /* d, after the check */Exercise 4 — write the guard. Given int scale_reading(int *reading_out, int raw); which must fail cleanly if handed a null pointer, write the first three lines of the function and say what it should return.
Exercise 5 — find the bug. This is meant to report the selected value, or say that nothing was selected. Say what is wrong, what Chapter 2.3 calls the underlying problem, and give the one-token repair.
int *selected;
if (mode == 1) {
selected = &primary;
}
if (selected != NULL) {
printf("%d\n", *selected);
}Exercise 6 — explain the silence. A colleague argues that their code has no null-pointer bugs because it compiles with -Wall -Wextra -Wpedantic and no warnings. Give the counterexample from §4, and say what would actually test their claim.
Exercise 7 — correct the embedded reasoning. An engineer on a bare-metal target says "a null dereference is harmless to debug because it always faults immediately." Explain which part of that is operating-system behaviour, what might happen instead on their target, and what they should do about it.
Exercise 8 — compare the spellings. Write the same declaration three ways — using NULL, using 0, and using nullptr — say which standard each needs, and say which you would write in a project building as C17 today and considering C23 later.
10. Summary
NULLmeans this pointer identifies no object, and is guaranteed to compare unequal to every real address.- It is not "address zero"; the representation is unspecified, and
0x0is what implementations typically print. - A constant
0in a pointer context is a null pointer constant. if (p)isif (p != NULL)— Chapter 2.10's truth model, unchanged.- Dereferencing one is undefined behaviour, and "it segfaults" is the operating system, not C.
- The compiler does not warn, even in the most obvious case — so the discipline is yours.
- Check at the boundary, once, where the pointer could be null; do not check pointers that cannot be.
- Initialise to
NULLwhen there is nothing to point at.nullptris C23's spelling. -fsanitize=undefinedreports null dereferences and array overruns with one flag.
The table in §5 said that a pointer arriving as a parameter is the case that most often needs checking, and did not explain why a function would be given one at all. That is the next chapter, and it is where this module repays its largest debt: Chapter 5.3 proved that a function cannot change a caller's object and could offer no remedy, and Chapter 5.4 listed the out-parameter among the ways a result can leave a function and had to mark it unavailable. Both become straightforward the moment a function can be handed an address.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 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.
