Why this matters
A pointer is a variable that holds an address, the place in memory where another value is stored. Reading the value at that address means following the pointer. A pointer that holds no address is called a null pointer. In C++ you write it as nullptr. Following a null pointer is a serious error. The C++ standard calls this undefined behaviour, which means the standard does not say what happens. In practice the program usually crashes at once. Sometimes it keeps running and prints a wrong answer, and that is harder to trace.
Conditions are where this error often shows up. A condition such as p->size() > 0 both checks a value and follows the pointer p. If p is null, the check itself is the crash. The fix is to test for null first, in the same condition, and to rely on the rule that && stops as soon as the left side is false. The right side, which follows the pointer, then never runs for a null pointer.
The crash does not always show up where the mistake is. A read through a null pointer may crash at once. It may also return whatever bytes happen to sit at that address, and the program goes on with a wrong value. The compiler may even delete a later null check, because it assumes the pointer was already valid. A program can therefore pass most tests and fail in one run. The safe habit is to put the null check in the same condition as the use, so that the two cannot be separated by an edit.
This lesson covers the null check inside a condition, the order of the checks in && and ||, and the mistakes that cause crashes. The rule that && skips its right side when the left side is false is covered in the guards lesson of the operators module.
The idea: check first, then use
A pointer is made by taking the address of a variable with &. The value behind a pointer is read with *. Here is a small program. One pointer points at a number, and the other is null:
The output is:
found is above 5
doneEach condition has two parts joined by &&. The left part, found != nullptr, asks whether the pointer holds an address. The right part, *found > 5, reads the value at that address. For found, the left part is true, so the right part runs and reads 7. For missing, the left part is false. Because && stops at the first false part, the right part never runs, and the program never reads from address zero. The second if is skipped and the program goes on to print done.
Note that nullptr is a keyword with its own type. It is not the number 0. Write ptr != nullptr to test a pointer, and write ptr == nullptr to test that it is empty. Older code sometimes uses NULL or 0 for this. The lesson uses nullptr throughout because it is the form the modern standard defines.
Worked example 1: a function that accepts a pointer
A function that takes a pointer cannot know whether the caller passed a real value. The caller may pass nullptr, so the function must check. This example returns true when a string is longer than five characters. A missing string counts as "not long".
The output is:
word: true
none: falseThe condition in is_long has the same two parts as before. The left part, text != nullptr, is checked first. The right part, text->size() > 5, calls size() through the pointer. The arrow -> means "follow the pointer, then use this member", and it is shorthand for (*text).size(). For a null pointer the right part is skipped, so the call to size() never happens.
The boolalpha line tells cout to print true and false as words, not as the numbers 1 and 0. It has no effect on the logic.
Worked example 2: walking a chain of nodes
The next example is harder. A chain is a list where each node holds a value and a pointer to the next node. The last node points to nullptr, which marks the end. The function below counts how many nodes in a row, starting from the first, have a value below a limit. It has to stop at the end of the chain, and it must not read a value from a node that does not exist.
The output is:
chain 2,4,9,1 with limit 5: 2
empty chain: 0The chain in main is d, then a, then b, then c. The values are 2, 4, 9 and 1. The walk counts 2 and 4, then stops at 9, which is not below 5. The answer is 2. The walk never reaches the 1, because the loop ends at the first failing value.
The loop condition is the key line. On each pass, the left part asks whether node still points to a node. The right part reads node->value. When node becomes nullptr after the last node, the left part is false, and the right part is never run. Without this check, the loop would read a value from nullptr after the last node, which is the crash described in the mistakes section below.
The empty chain is passed as nullptr directly. The first check fails at once, and the function returns 0.
Worked example 3: a lookup that may find nothing
The last example is the most common pattern in real code. A function searches a list and returns a pointer to the match, or nullptr when nothing matches. The caller then has to check the result before using it. The conditions below check three things, in order: that a user was found, that the user is active, and that the user is 18 or older.
The output is:
Asha: true
Ravi: false
Meena: false
Dev: falseThe name Dev is not in the list, so find_user returns nullptr. The first part of the condition, user != nullptr, is false, and the two checks after it never run. Without that first part, user->active would be read from a null pointer.
Each part of the condition depends on the part before it. The active check reads through the pointer, so it is safe only because the null check ran first. The age check runs only when the active check is true. The order is therefore part of the logic, not a matter of style.
Worked example 4: a pointer inside a pointer
Some objects hold pointers to other objects. Each of those pointers can be null, so a condition that reaches through two or more pointers needs one null check for each level. This example asks whether a customer lives in Pune. The customer pointer can be null, and the address pointer inside the customer can be null too.
The output is:
Asha: true
Ravi: false
Meena: false
nobody: falseEach -> in the condition follows one pointer. The first check, c != nullptr, makes sure the customer exists. The second check, c->home != nullptr, makes sure the customer has an address, and only then does c->home->city run. For Meena, the address pointer is null, so the second check is false and the city is never read. Without the middle check, the last part would follow a null pointer and crash.
The rule is to write the checks in the same order as the arrows. Each pointer is checked before the first arrow that follows it.
The order of && and ||
The rule for the two operators is short. && stops at the first false part, so its right side runs only when the left side is true. || stops at the first true part, so its right side runs only when the left side is false.
For a null check, this means the null test goes on the left of && when you want the rest to need a real pointer. It goes on the left of || when a null pointer should give the answer true straight away. The example below uses ||:
The output is:
null: true
empty: true
hello: falseFor a null pointer, the left part text == nullptr is true, so || stops there and the call to empty() never runs. For a real string, the left part is false, so the right part runs and checks whether the string is empty. In both forms, the pointer is used only after the null test has run.
Common mistakes
The check after the use
This is the mistake that causes most of these crashes. The pointer is used first and checked second, so the check comes too late.
The output is:
word: trueThe program stops after the first line, and the shell reports exit code 139. This code is a segmentation fault, the operating system's signal that the program touched memory it may not use. The first call works, because the pointer is valid. The second call crashes inside size(), because text is null when the function starts. The && runs its left side first, so size() is called before the null check on the right can run. The line that prints not reached never prints.
The loop from worked example 2 shows the same mistake in a case that works sometimes. Here the check on the value comes before the null check:
The output is:
chain 2,4,9,1 with limit 5: 2The first call returns the right answer, because the walk stops at 9 before it reaches the end of the chain. The second call starts at a chain of one node. Its value 1 is below the limit, so the loop body runs, and node becomes nullptr. The loop then checks node->value before the null test, and the program crashes with exit code 139. The bug is hidden on the first call and visible on the second. A test that covers only the first case would pass.
Using || where && is needed
The operators are easy to confuse. The next function is meant to say whether a user exists and is active. The author wrote || instead of &&:
The output is:
Meena: trueMeena exists but is not active, so the answer should be false. The || sees that user != nullptr is true, and it stops there. The user->active part never runs, so the inactive status is ignored. Calling this with a name that is not in the list would be worse: the left part would be false, and user->active would then read a null pointer.
Assignment where a comparison was meant
A single = assigns a value. A double == compares. Writing one in place of the other compiles without an error, and the result is often wrong:
The output is:
pointer is nullThe condition p = nullptr sets p to null and then uses the result. A null pointer is false in a condition, so the else branch runs. The program has now lost its pointer to value. The compiler warns about this with -Wall:
main.cpp:7:11: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
7 | if (p = nullptr) {
| ~~^~~~~~~~~Treat this warning as an error. If the assignment is really wanted, put it in its own line and compare on the next one.
Comparing a pointer with a number
p == 5 compares the address stored in p with the number 5. That is almost never what you want. The value the pointer points to is *p, so the comparison is *p == 5. The compiler rejects the first form:
#include <iostream>
using namespace std;
int main() {
int value = 5;
int* p = &value;
if (p == 5) {
cout << "match" << endl;
}
}The compiler stops with this error:
main.cpp: In function ‘int main()’:
main.cpp:7:11: error: ISO C++ forbids comparison between pointer and integer [-fpermissive]
7 | if (p == 5) {
| ~~^~~~Write *p == 5 for the value, and p != nullptr for the address. Writing p == 0 compiles, but it hides the intent, so use nullptr for the address test.
Checklist before you write a pointer condition
- List the pointers in the condition, and mark each one that can be null. Parameters, search results and fields of other objects often can be.
- Put the null test for each pointer on the left of the first part that uses it.
- Use
&&when every part must hold, and||when one part is enough. Check which side each pointer use sits on. - Count the arrows. Each
->follows one pointer, so each pointer on the way needs its check first. - Test the null case. A test that passes only real objects will not catch the crash.
Practice
Exercise 1
Write a function has_first_a that takes a const string* and returns true when the pointer is not null, the string is not empty, and its first character is a. Put the checks in one condition, in the safe order.
Show the answer
bool has_first_a(const string* s) {
return s != nullptr && !s->empty() && s->front() == 'a';
}The null check is first, so the calls to empty() and front() run only for a real string. front() returns the first character. The test !s->empty() is needed because front() on an empty string is not valid. Called with apple, the answer is true. Called with kiwi, it is false. Called with nullptr, it is false.
Exercise 2
What goes wrong with this condition, and how do you fix it?
if (node->next != nullptr && node != nullptr)Show the answer
The left part reads node->next before the right part checks node. If node is null, the program crashes on the left part. The fix is to swap the two parts so that node != nullptr runs first:
if (node != nullptr && node->next != nullptr)Exercise 3
A function is written like this:
bool big(const int* p) {
return p == nullptr || *p > 10;
}Predict the result when it is called with nullptr, with a pointer to the number 4, and with a pointer to the number 20.
Show the answer
With nullptr, the result is true. The left part p == nullptr is true, so || stops and *p is never read. With a pointer to 4, the left part is false, and *p > 10 is false, so the result is false. With a pointer to 20, the result is true. The function treats a missing number as big, and the answer depends on the order of the two parts.
Exercise 4
Write a function count_positive that takes a const vector<int>* and returns how many of its numbers are greater than 0. When the pointer is null, it returns 0. Use one if to test for null before the loop.
Show the answer
int count_positive(const vector<int>* v) {
int count = 0;
if (v != nullptr) {
for (int x : *v) {
if (x > 0) {
++count;
}
}
}
return count;
}With the list 3, -1, 0 and 8, the function returns 2. With nullptr, it returns 0. The loop reads *v only inside the if, so the null case never reaches it.
Exercise 5
What is wrong with this condition, and how do you fix it?
if (p == 5 && *p > 2)Show the answer
The left part compares the address in p with the number 5. That is not allowed for a pointer, and the compiler stops with an error. The left part is also missing a null check. If the intent was to test the value, a fixed version is:
if (p != nullptr && *p > 2)The null check is first, so *p is read only for a real pointer.
Summary
- A null pointer holds no address. Following it, with
->or*, is undefined behaviour and usually a crash. - Put
ptr != nullptron the left of&&, so the checks that follow the pointer run only when it is valid. - Put
ptr == nullptron the left of||when a null pointer should give an answer straight away. - A loop that walks a chain of pointers needs the null check in its loop condition, before any read through the pointer.
- A bug that crashes only on some inputs can pass a test that covers the other inputs, so test an empty or null case too.