As you learned in the last lesson (8.6 -- Pointers), pointers are variables that hold an address. This address can be dereferenced using the dereference operator (*) to get the value at that address.
Pointers can be initialized to hold the address of another variable:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> int main() { int x{ 5 }; int* ptr{ &x }; // initialized pointer std::cout << *ptr; // prints 5 return 0; } |
An uninitialized pointer is called a wild pointer
, and will contain a garbage address. Dereferencing a wild pointer will result in undefined behavior.
1 2 3 4 5 6 7 8 9 |
#include <iostream> int main() { int* ptr; // a wild pointer, contains a garbage address std::cout << *ptr; // undefined behavior return 0; } |
A pointer that is pointing to object that is no longer valid (e.g. because the object has been destroyed) is called a dangling pointer
. Dereferencing a dangling pointer will also result in undefined behavior.
Null pointers
Besides a memory address, there is one additional value that a pointer can hold: a null value. A null value (often shortened to null) is a special value that means the pointer is not pointing at anything. A pointer holding a null value is called a null pointer.
The easiest way to create a null pointer is to use value initialization to zero initialize the pointer:
1 2 3 4 5 6 |
int main() { int* ptr {}; // ptr is now a null pointer, and is not holding an address return 0; } |
Rule
Zero initialize your pointers (to be null pointers) if you are not initializing them with the address of a valid object.
The nullptr keyword
C++11 introduced a new keyword named nullptr
. Much like true
and false
are keywords that represent Boolean literal values, nullptr is a keyword that represents a null pointer value.
1 2 3 4 5 6 7 8 9 10 |
int main() { int value { 5 }; int* ptr { nullptr }; // can use nullptr to initialize a pointer to be a null pointer int* ptr2 { &value }; ptr2 = nullptr; // use nullptr when assigning a null pointer return 0; } |
Although we haven’t discussed pointers as function parameters yet, nullptr
should also be preferred when passing a null pointer literal.
1 |
someFunction(nullptr); // prefer nullptr when passing a literal null pointer to a function |
Best practice
Use nullptr
when you need a null pointer literal for initialization, assignment, or passing to a function.
Pointers and Boolean conversions
If you recall from lessons of yore, integer values can be converted into to Boolean values: an integer value of 0
converts to Boolean value false
, and any other integer value converts to Boolean value true
.
Similarly, pointers can also be converted to Boolean values: a null pointer converts to Boolean value false
, and a non-null pointer converts to Boolean value true
.
Therefore, we can use a conditional (or the conditional operator) to test whether a pointer is a null pointer or not:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> int main() { int x { 5 }; int* ptr { &x }; int* nullPtr {}; // pointers convert to Boolean false if they are null, and Boolean true if they are non-null if (ptr) std::cout << "ptr is non-null\n"; else std::cout << "ptr is null\n"; std::cout << "nullPtr is " << (nullPtr ? "non-null\n" : "null\n"); return 0; } |
The above program prints:
ptr is non-null nullPtr is null
Warning
Conditionals can only be used to differentiate null pointers from non-null pointers. There is no convenient way to determine whether a non-null pointer is pointing to a valid or invalid object.
Therefore, care needs to be taken to ensure that your pointers always either have the address of a valid object or are set to null (when not pointing to a valid object).
Dereferencing null pointers
Much like dereferencing a wild or dangling pointer leads to undefined behavior, dereferencing a null pointer also leads to undefined behavior. In most cases, it will crash your application.
The following program illustrates this, and will probably crash or terminate abnormally when you run it (go ahead, try it, you won’t harm your machine):
1 2 3 4 5 6 7 8 9 |
#include <iostream> int main() { int* ptr {}; // Create a null pointer std::cout << *ptr; // Dereference the null pointer return 0; } |
Conceptually, this makes sense. Dereferencing a pointer means “go to the address the pointer is pointing at and access the value there”. A null pointer doesn’t have an address. So when you try to access the value at that address, what should it do?
Accidentally dereferencing null or garbage pointers is one of the most common mistakes the C++ programmers make, and is probably the most common reason that C++ programs crash in practice.
Warning
Care needs to be taken to ensure pointers are non-null before dereferencing them or undefined behavior will result.
Legacy null pointer literals: 0 and NULL
In older code, you may see two other values used instead of nullptr
.
The first is the literal 0
. In the context of a pointer, the literal 0
is specially defined to mean a null pointer, and is the only time you can assign an integer literal to a pointer.
1 2 3 4 5 6 7 8 9 |
int main() { float* ptr { 0 }; // ptr is now a null pointer float* ptr2; // ptr2 is uninitialized ptr2 = 0; // ptr2 is now a null pointer return 0; } |
As an aside...
On modern architectures, the address 0 is typically used to represent a null pointer. However, this is not guaranteed by the C++ standard, and some architectures use other values. The literal 0, when used in the context of a null pointer, will be translated into whatever address the architecture uses to represent a null pointer.
Additionally, there is a preprocessor macro named NULL (defined in the <cstddef> header). This macro is inherited from C, where it is commonly used to indicate a null pointer.
1 2 3 4 5 6 7 8 |
#include <cstddef> // for NULL int main() { double* ptr { NULL }; // ptr is a null pointer double* ptr2; // ptr2 is uninitialized ptr2 = NULL; // ptr2 is now a null pointer |
Both 0
and NULL
should be avoided in modern C++ (use nullptr
instead).
For advanced readers
In this subsection, we’ll explain why using 0 or NULL is no longer preferred.
Note that the value of 0
isn’t a pointer value (it’s an integer literal), so assigning 0 (or NULL in some cases) to a pointer to denote that the pointer is a null pointer is a little inconsistent. In rare cases, when used as a literal argument, it can even cause problems because the compiler can’t tell whether we mean a null pointer value or the integer value 0
.
We haven’t talked about this yet, but it turns out that you can define multiple functions with the same name so long as they take parameters of different types. The compiler can tell which one you want by the arguments passed in.
When using integer values, this can cause problems:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <iostream> #include <cstddef> // for NULL void print(int x) // this function accepts an integer { std::cout << "print(int): " << x << '\n'; } void print(int* ptr) // this function accepts an integer pointer { std::cout << "print(int*): " << (ptr ? "non-null\n" : "null\n"); } int main() { int x { 5 }; int* ptr { &x }; print(ptr); // always calls print(int*) because x has type int* (good) print(0); // always calls print(int) because 0 is an integer literal (may or may not be what we expect) print(NULL); // most likely calls print(int), but could call print(int*) depending on how NULL is defined (definitely not what we want) print(nullptr); // always calls print(int*) because nullptr only converts to a pointer type (good) return 0; } |
On the author’s machine, this prints:
print(int*): non-null print(int): 0 print(int): 0 print(int*): null
Note that when passing integer value 0
as a parameter, C++ prefers print(int)
over print(int*)
. This can lead to unexpected results.
In the likely case where NULL is defined as value 0, print(NULL) will call print(int), not print(int*) like you might expect for a null pointer literal.
std::nullptr_t
One interesting question: if nullptr
can be differentiated from integer value 0
, it must have a different type. What type is nullptr
? The answer is that nullptr
has type std::nullptr_t
(defined in header <cstddef>). std::nullptr_t
can only hold one value: nullptr
! While this may seem kind of silly, it’s useful in one situation. If we want to write a function that accepts only a nullptr
argument, what type do we make the parameter? The answer is std::nullptr_t
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> #include <cstddef> // for std::nullptr_t void doSomething(std::nullptr_t ptr) { std::cout << "in doSomething()\n"; } int main() { doSomething(nullptr); // call doSomething with an argument of type std::nullptr_t return 0; } |
You probably won’t ever need to use this, but it’s good to know, just in case.
Favor references over pointers whenever possible
Pointers are more flexible than references, as they can be changed to point at other addresses, or set to point at nothing. This means null pointers are common, and dangling pointers are easy to inadvertently create. Failure to guard against dereferencing such pointers will result in application crashes.
References are safer, and should be favored unless the additional flexibility provided by pointers is required.
Best practice
Favor references over pointers unless the additional flexibility provided by pointers is needed.
![]() |
![]() |
![]() |
Leave a Reply