Why this matters
A program that crashes at once is easy to fix. A program that crashes only sometimes, or that prints a wrong number without any error, is much harder. Many of the worst bugs in C++ come from two kinds of bad pointer. A null pointer points at nothing. A dangling pointer points at memory that was already given back, so the address still looks valid but no longer holds the value you put there.
Java has a related bug. A null reference points at nothing, and using it stops the program with an error. Java does not have dangling references in normal code, and this lesson shows why.
This lesson assumes you know what a pointer is. The earlier lessons in this module cover addresses, the stack and the heap, and smart pointers. Here we look at the moment a pointer is used while it is wrong, and at the checks that stop that moment from happening.
Three words you need
A pointer is a variable that holds the address of another value. An address is a number that tells the computer where a value sits in memory, in the same way a house number tells a visitor where a house is. The * operator reads or writes the value at an address. This is called dereferencing. The expression *p means "the value at the address stored in p".
A null pointer holds the special value nullptr, which means "no address". Reading through it is undefined behaviour, and in practice the program usually crashes.
A dangling pointer holds an address that was valid once, but is no longer valid. The memory was given back with delete, or the variable it pointed at has already ended. The name is an image: a rope left hanging loose after the thing it was tied to is gone.
C++ calls the result of using such a pointer undefined behaviour. The language standard does not say what happens. The program may crash, print a random number, or appear to work. The same code can change its behaviour after a compiler upgrade or a change in the optimisation setting.
Null: a pointer that points at nothing
Write nullptr for a null pointer. Older code writes NULL or 0, but nullptr is clearer, and the compiler will not confuse it with a whole number.
Set a pointer to nullptr when you declare it and you have no address yet. An uninitialised pointer is worse than a null one. Its bits hold whatever was in that memory before, so it looks like an address and nothing warns you that it is wrong.
The main check for a null pointer is a comparison with nullptr. Make the comparison before every read through the pointer.
Worked example 1: a null check in a function
This function reads a value through a pointer. It returns 0 when the pointer is null, so the caller never reads through a null pointer.
#include <iostream>
// Returns the value at p, or 0 when p is null.
int value_or_zero(const int* p) {
if (p == nullptr) {
return 0;
}
return *p;
}
int main() {
int score = 90;
int* missing = nullptr;
std::cout << "score: " << value_or_zero(&score) << "\n";
std::cout << "missing: " << value_or_zero(missing) << "\n";
return 0;
}Trace it by hand. The first call passes the address of score. The check p == nullptr is false, so the function returns *p, which is 90. The second call passes missing, which holds nullptr. The check is true, so the function returns 0 and never touches memory at address zero. The output is:
score: 90
missing: 0Worked example 2: a list that ends in null
A linked list is a chain of small objects. Each object, called a node, holds a value and a pointer to the next node. The last node points at nullptr, which is how the code knows the chain has ended. Every loop over a list needs that stop check.
#include <iostream>
struct Node {
int value;
Node* next;
};
int main() {
Node third{30, nullptr};
Node second{20, &third};
Node first{10, &second};
Node* cur = &first;
while (cur != nullptr) {
std::cout << cur->value << "\n";
cur = cur->next; // the last node has next == nullptr, so the loop stops
}
Node* missing = nullptr;
if (missing != nullptr && missing->value > 0) {
std::cout << "positive\n";
} else {
std::cout << "skipped: the check stopped the read\n";
}
return 0;
}The && operator stops early. When missing != nullptr is false, C++ does not evaluate missing->value > 0, so the read never happens. The output is:
10
20
30
skipped: the check stopped the readThe -> operator is shorthand for dereferencing and then reading a field. cur->value means (*cur).value. Both forms read through the pointer, so both need the check first.
What a null read looks like
Here is a program that reads through a null pointer with no check.
#include <iostream>
int main() {
int* p = nullptr;
std::cout << "about to read through a null pointer\n";
std::cout << *p << "\n";
return 0;
}Compile it with g++ -std=c++20 -Wall null_crash.cpp -o null_crash and run it. The program stops with an exit code of 139. On Linux, an exit code above 128 means the program ended because of a signal. 139 is 128 plus 11, and signal 11 is SIGSEGV, the signal for an invalid memory access. The shell may print Segmentation fault, and the exact text depends on your shell.
The line about to read through a null pointer may not appear. When the output goes to a pipe, std::cout holds its text in a buffer until the buffer is flushed, and the crash happens first. Writing std::endl instead of "\n" flushes the buffer, which is one reason to use it when debugging a crash.
Build with the AddressSanitizer option to see a clearer report. The AddressSanitizer is a tool built into g++ and clang that checks memory use while the program runs. Compile with g++ -std=c++20 -g -fsanitize=address null_crash.cpp -o null_asan. One run printed this first part of the report:
AddressSanitizer:DEADLYSIGNAL
==748415==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x58f4e32ea271 bp 0x7ffc16eb7bb0 sp 0x7ffc16eb7ba0 T0)
==748415==The signal is caused by a READ memory access.
==748415==Hint: address points to the zero page.
#0 0x58f4e32ea271 in main null_crash.cpp:6The address 0x000000000000 is the null address. The report says the read happened on line 6, which is the line with *p. The process number after == and the other addresses change on every run, so your report will show different numbers.
Dangling pointers: an address that is no longer valid
A dangling pointer usually comes from one of two patterns.
The first pattern is reading after delete. The delete operator gives heap memory back to the system, so the memory may be reused for something else. Deleting the memory does not change the pointer. The pointer still holds the old address, and it is not null.
The second pattern is keeping the address of a local variable after the function that owns it has returned. A local variable is a variable declared inside a function. It lives only while that function runs. Its memory is reclaimed when the function returns.
Worked example 3: reading after delete
#include <iostream>
int main() {
int* p = new int(7);
std::cout << "value: " << *p << "\n";
delete p;
p = nullptr; // forget the address: the pointer now says "nothing here"
if (p != nullptr) {
std::cout << "value: " << *p << "\n";
} else {
std::cout << "p is null, not reading\n";
}
return 0;
}Here the pointer is set to nullptr right after delete, so the check at the end is false, and the program prints a safe message. The output is:
value: 7
p is null, not readingNow remove the line p = nullptr; and read through p after the delete. This is the dangling version:
#include <iostream>
int main() {
int* p = new int(7);
delete p;
// Bug: p still holds the old address. This read is undefined behaviour.
std::cout << "value after delete: " << *p << "\n";
return 0;
}The compiler gives no warning for this program, even with -Wall. Compiled normally and run three times, it printed three different numbers:
value after delete: -893024073
value after delete: 1583970630
value after delete: -759229182The value 7 is gone, and the output changes from run to run. A program can look fine in one test and fail in another. This is why the bug is hard to find without a tool.
Worked example 4: the same bug, caught by the sanitizer
Compile the dangling version with the sanitizer:
g++ -std=c++20 -g -Wall -fsanitize=address ex3_dangling_read.cpp -o ex3a
./ex3aThe first lines of the report were:
=================================================================
==750406==ERROR: AddressSanitizer: heap-use-after-free on address 0x6f8f52de0010 at pc 0x651a85c6a340 bp 0x7fffdc5cb980 sp 0x7fffdc5cb970
READ of size 4 at 0x6f8f52de0010 thread T0
#0 0x651a85c6a33f in main ex3_dangling_read.cpp:7Read the first line of the error. heap-use-after-free means the program read heap memory after it was freed. The READ of size 4 line says the read was four bytes, the size of an int. The #0 line names the file and the line, here line 7. The report goes on to show two more stacks: where the memory was freed, and where it was first allocated. Those two places are what you need to fix the code.
The sanitizer is slower and uses more memory, so use it while you develop and test. Do not ship a program built with it.
Worked example 5: a pointer to a local variable
A function can return a pointer, but the pointer must not point at one of the function's own local variables.
#include <iostream>
int* make_number() {
int local = 5; // lives only until make_number returns
return &local; // bug: the address outlives the variable
}
int main() {
int* p = make_number();
std::cout << "pointer is " << (p == nullptr ? "null" : "not null") << "\n";
return 0;
}Compile it with g++ -std=c++20 -Wall -Wextra ex4_return_local.cpp. g++ 15.2 warns:
ex4_return_local.cpp: In function ‘int* make_number()’:
ex4_return_local.cpp:5:12: warning: address of local variable ‘local’ returned [-Wreturn-local-addr]
5 | return &local; // bug: the address outlives the variable
| ^~~~~~
ex4_return_local.cpp:4:9: note: declared here
4 | int local = 5; // lives only until make_number returns
| ^~~~~
pointer is nullThe warning is the useful part. The last line is not a promise. In this build, both at the default optimisation level and with -O2, the returned value was null, so the caller saw a null pointer. Another compiler or setting could return the old address, and then a read would be a dangling read. The fix is to return the value itself, or to return a value that owns its memory, such as a std::vector or a smart pointer from the earlier lesson.
Checking before use
Use these checks in your own code.
- Compare with
nullptrbefore every read or write through a pointer that may be null. Writeif (p != nullptr)before*porp->. - When you
deletea pointer you own, set it tonullptron the next line. Later code can then test it. - Setting
p = nullptrclears onlyp. If you made a second copy of the address, such asint* q = p;, that copy still points at the freed memory. Clear every copy, or avoid copies by having one owner. - Do not return the address of a local variable. Return the value, or give the caller an object that owns the memory.
- Use a reference when the object always exists. A reference must be attached to an object when it is declared, and normal code cannot make it null. Use a pointer when the object may be missing.
- Compile with
-Wall -Wextra, and read every warning. The-Wreturn-local-addrwarning in worked example 5 is one of many that point at pointer bugs. - While testing, build with
-fsanitize=address, and run the tests with it. The report points at the line that used the bad pointer.
A null check cannot catch a dangling pointer. The check asks "is this address zero?". A freed address is not zero, so the check passes and the read still happens. Only the habit of clearing pointers on delete, and the tools above, catch that case.
Java: null references
Java does not have pointers that you can do arithmetic on, and it does not have delete. A Java reference is a handle to an object. Java's garbage collector finds objects that the program can no longer reach and frees them. Because of this, a Java program cannot hold a reference to memory that has been freed in normal code. The problem that remains is the null reference, which is a reference that points at no object.
Using a null reference with a dot, such as name.length(), throws a NullPointerException. An exception is Java's way of stopping the normal flow and reporting an error. If nothing catches it, the program prints a message and stops.
Worked example 6: the Java crash
public class NullDemo {
static String findName(int id) {
if (id == 1) {
return "Asha";
}
return null;
}
public static void main(String[] args) {
String name = findName(2);
System.out.println("name is " + name);
System.out.println("length is " + name.length());
}
}The method returns null for id 2. The first println prints name is null. The second line tries to call length() on a null reference. Compile with javac NullDemo.java and run with java NullDemo. The output is:
name is null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
at NullDemo.main(NullDemo.java:12)Recent Java versions print a helpful message. It names the method that could not be called and the variable that was null. Here the variable is shown as <local1>, because the default javac settings do not store local variable names in the class file. Compiling with javac -g NullDemo.java stores the names, and the message then says because "name" is null. The number after at is the line of the failing call, which is line 12 here.
Worked example 7: the Java check
public class SafeDemo {
static String findName(int id) {
if (id == 1) {
return "Asha";
}
return null;
}
public static void main(String[] args) {
String name = findName(2);
if (name == null) {
System.out.println("no name found");
} else {
System.out.println("length is " + name.length());
}
}
}The check name == null is the Java version of p != nullptr. The output is:
no name foundThe program now behaves the same way every time, and it has a message that explains what happened.
How the two languages differ
In C++, a null read usually crashes with a signal, and a dangling read may print garbage or appear to work. In Java, a null read throws an exception with a message that names the variable, and the program can catch it. In C++ you must check every pointer yourself. In Java you must still check for null, but a freed object cannot be reached through a reference, so the garbage-related bugs of C++ do not occur.
Common mistakes and the messages you see
Each mistake below has a message that tells you what to look for.
-
Using a function result without checking it. A function that searches for a value often returns
nullptrornullwhen the value is missing. The caller must test the result. Java reports this asNullPointerException: Cannot invoke ... because "<local1>" is null. C++ may report a segmentation fault and exit code 139. -
Reading after
delete. The program may print a random number and exit with code 0. Only a sanitizer or a close reading of the code will show the bug. The message from the sanitizer isheap-use-after-free. -
Clearing one copy of a pointer. Setting
p = nullptrdoes not changeq. The check onppasses, and the read throughqis still a dangling read. -
Returning the address of a local. g++ prints
warning: address of local variable ... returned [-Wreturn-local-addr]. Treat that warning as an error in your code. -
Trusting a null check to catch everything. A null check catches only the address zero. It cannot tell whether a non-zero address is still valid.
-
Testing in the wrong order. Write
p != nullptr && p->value > 0, notp->value > 0 && p != nullptr. The second form reads throughpbefore the check runs.
Practice
Exercise 1: predict the output
What does this program print?
#include <iostream>
int safe_read(const int* p) {
return p != nullptr ? *p : -1;
}
int main() {
int n = 12;
std::cout << safe_read(&n) << "\n";
std::cout << safe_read(nullptr) << "\n";
return 0;
}Show the answer
The first call passes the address of n, which holds 12. The second call passes nullptr, so the condition is false and the function returns -1. The output is:
12
-1Exercise 2: find the bug
This function is meant to free a number and then print that the number is gone. What is wrong with it?
#include <iostream>
void release(int* p) {
delete p;
std::cout << "freed, value was " << *p << "\n";
}Show the answer
The read *p happens after delete p. The memory has been given back, so the read is a dangling read and its result is undefined. The fix is to print before deleting, or to skip the read. A simple corrected version is:
#include <iostream>
void release(int*& p) {
std::cout << "freeing value " << *p << "\n";
delete p;
p = nullptr;
}The reference parameter int*& p lets the function set the caller's pointer to nullptr. The caller can then check the pointer before any later use.
Exercise 3: walk a list with a null check
Given the list 1, 2 and then a null end, what does this code print?
#include <iostream>
struct Node {
int value;
Node* next;
};
int main() {
Node b{2, nullptr};
Node a{1, &b};
Node* head = &a;
int total = 0;
for (Node* cur = head; cur != nullptr; cur = cur->next) {
total += cur->value;
}
std::cout << "total " << total << "\n";
Node* missing = head->next->next;
if (missing != nullptr) {
std::cout << missing->value << "\n";
} else {
std::cout << "no third node\n";
}
return 0;
}Show the answer
The loop adds 1 and 2, so the total is 3. Then head->next is &b, and b.next is nullptr, so missing is nullptr. The check fails and the program prints the other message:
total 3
no third nodeExercise 4: the Java version of the check
Change SafeDemo so that it prints the length of a name when the name is present, and prints 0 otherwise, using the ternary operator ? : on one line. Write the line.
Show the answer
Use the test in the same way as the if form:
System.out.println("length is " + (name == null ? 0 : name.length()));When name is null, the test is true and the value is 0. When name is not null, the call to length() runs. The name.length() part is never evaluated for a null name, so no exception is thrown.
Exercise 5: a copy that keeps a dangling address
What does this program print, and why?
#include <iostream>
int main() {
int* p = new int(3);
int* q = p;
delete p;
p = nullptr;
std::cout << "p is " << (p == nullptr ? "null" : "set") << "\n";
std::cout << "q is " << (q == nullptr ? "null" : "set") << "\n";
return 0;
}Show the answer
The program prints:
p is null
q is setp was set to nullptr, but q was a copy made before the delete. It still holds the old address, which is not zero, so the check reports "set". Reading *q would be a dangling read. The fix is to clear q too, or to have only one pointer that owns the memory.
Summary
- A null pointer holds
nullptr. Test it withp != nullptrbefore every read through it. - A dangling pointer holds an address that was freed or whose variable has ended. Reading through it is undefined behaviour, and the result may look correct.
- Clear a pointer to
nullptrafterdelete, and clear every copy of it. - Never return the address of a local variable. Read the
-Wreturn-local-addrwarning and fix the code. - Java throws
NullPointerExceptionfor a null reference, and its message names the variable when the class file keeps the name. - While testing C++, build with
-g -fsanitize=addressto get the line that used the bad pointer.