[2026] C++ nullptr | Null Pointer Guide
이 글의 핵심
Complete guide to C++11 nullptr: differences from NULL and 0, function overloading, nullptr_t, and migration strategies.
🎯 What You’ll Learn (Reading Time: 13 minutes)
TL;DR: Master C++11 nullptr and understand why it’s necessary. Learn how nullptr solves the problems of NULL and 0, and write type-safe pointer code. What you’ll learn:
- ✅ Perfect understanding of nullptr vs NULL vs 0 differences
- ✅ Master nullptr’s role in function overloading
- ✅ Gain ability to write type-safe pointer code Practical Applications:
- 🔥 Legacy code migration (NULL → nullptr)
- 🔥 Prevent function overloading bugs
- 🔥 Improve template code type safety Difficulty: Beginner | C++11 Required | Practice Code: 8 examples
What is nullptr?
nullptr is a type-safe null pointer literal introduced in C++11. It solves the problems of NULL and 0, providing a clear null value that can only be used with pointer types.
// Before C++03
int* p1 = NULL; // Macro (usually 0)
int* p2 = 0; // Integer 0
// After C++11
int* p3 = nullptr; // Type-safe
Why is it needed?:
- Type Safety: Prevents confusion between integers and pointers
- Overload Resolution: Clear selection in function overloading
- Template Safety: Solves type deduction problems in templates
- Clarity: Clearly expresses code intent
// ❌ NULL's problem: interpreted as integer
#define NULL 0
void func(int x) { std::cout << "int\n"; }
void func(int* ptr) { std::cout << "pointer\n"; }
func(NULL); // Outputs "int" (not intended!)
// ✅ nullptr: clearly interpreted as pointer
func(nullptr); // Outputs "pointer" (as intended)
nullptr, NULL, 0 — What’s the Difference?
Only nullptr is a literal specialized for “pointer null”. 0 is an integer literal, and NULL is usually a macro defined as 0, so it overlaps with integer overloads in priority.
| Expression | Meaning | Selects f(int) | Pointer Intent | Assign to int variable |
|---|---|---|---|---|
0 | Integer 0 or null pointer constant | Possible | Converts to pointer depending on context | int x = 0 OK |
NULL | Implementation-defined (0, 0L, etc.) | Often integer side | May differ from intent | Possible depending on implementation (warning) |
nullptr | std::nullptr_t | Generally No | Matches pointer/nullptr_t overload | Impossible (type-safe) |
Since NULL definition can be (void*)0 in some environments and simple 0 in others, overload resolution can vary, so nullptr is advantageous for both portability and readability. |
Function Overloading Problem (Advanced)
APIs that accept both int and int* are common in legacy code. In this case, NULL and 0 tend to bind to the “integer” side, and bugs occur where the int overload is called when the intent is “no pointer”. nullptr is passed as std::nullptr_t, so it matches well with pointer-related overloads.
If you have a std::nullptr_t-specific overload, you can separate a branch that handles “only null literals”.
#include <cstddef>
#include <iostream>
// Example execution
void f(int) { std::cout << "int\n"; }
void f(int*) { std::cout << "int*\n"; }
void f(std::nullptr_t) { std::cout << "nullptr_t\n"; }
int main() {
f(0); // Usually int
f(NULL); // Usually int (implementation-dependent)
f(nullptr); // nullptr_t overload
int* p = nullptr;
f(p); // int*
}
Type of nullptr:
The type of nullptr is std::nullptr_t. It can be implicitly converted to all pointer types, but cannot be converted to integer types.
#include <cstddef>
// Type of nullptr
using nullptr_t = decltype(nullptr);
int* p1 = nullptr; // OK: converts to pointer
char* p2 = nullptr; // OK: converts to pointer
void* p3 = nullptr; // OK: converts to pointer
// int x = nullptr; // Error: cannot convert to integer
How nullptr Works:
nullptr is a prvalue of the special type std::nullptr_t. The compiler implicitly converts nullptr to all pointer types, but does not convert it to integer types.
// Conceptual implementation
struct nullptr_t {
// Can convert to all pointer types
template<typename T>
operator T*() const { return 0; }
// Can also convert to member pointers
template<typename C, typename T>
operator T C::*() const { return 0; }
// Cannot convert to int
operator int() const = delete;
};
const nullptr_t nullptr = {};
Problems with NULL
#define NULL 0
void func(int x) {
cout << "int: " << x << endl;
}
void func(int* ptr) {
cout << "pointer" << endl;
}
int main() {
func(NULL); // Ambiguous! int version called
func(nullptr); // pointer version called
}
Advantages of nullptr
// 1. Type Safety
int* p = nullptr; // OK
int x = nullptr; // Error: cannot assign to integer
// 2. Overload Resolution
void func(int);
void func(char*);
func(0); // int version
func(NULL); // int version (problem!)
func(nullptr); // char* version (as intended)
// 3. Safe in Templates
template<typename T>
void process(T* ptr) {
if (ptr == nullptr) {
// ...
}
}
Basic Usage
int* ptr = nullptr;
// nullptr check
if (ptr == nullptr) {
cout << "null pointer" << endl;
}
if (!ptr) {
cout << "null pointer" << endl;
}
// Function argument
void func(int* p = nullptr) {
if (p) {
*p = 10;
}
}
Practical Examples
Example 1: Safe Pointer Usage
class Node {
public:
int data;
Node* next;
Node(int d) : data(d), next(nullptr) {}
};
class LinkedList {
private:
Node* head;
public:
LinkedList() : head(nullptr) {}
~LinkedList() {
while (head != nullptr) {
Node* temp = head;
head = head->next;
delete temp;
}
}
void append(int data) {
Node* newNode = new Node(data);
if (head == nullptr) {
head = newNode;
return;
}
Node* current = head;
while (current->next != nullptr) {
current = current->next;
}
current->next = newNode;
}
void print() const {
Node* current = head;
while (current != nullptr) {
cout << current->data << " -> ";
current = current->next;
}
cout << "nullptr" << endl;
}
};
int main() {
LinkedList list;
list.append(1);
list.append(2);
list.append(3);
list.print();
}
Example 2: Optional Parameters
class Logger {
private:
ofstream* file;
public:
Logger(const string* filename = nullptr) : file(nullptr) {
if (filename != nullptr) {
file = new ofstream(*filename);
}
}
~Logger() {
if (file != nullptr) {
file->close();
delete file;
}
}
void log(const string& message) {
if (file != nullptr) {
*file << message << endl;
} else {
cout << message << endl;
}
}
};
int main() {
// File logging
string filename = "log.txt";
Logger fileLogger(&filename);
fileLogger.log("Write to file");
// Console logging
Logger consoleLogger;
consoleLogger.log("Output to console");
}
Example 3: Factory Pattern
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() const override {
cout << "Circle" << endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
cout << "Rectangle" << endl;
}
};
Shape* createShape(const string& type) {
if (type == "circle") {
return new Circle();
} else if (type == "rectangle") {
return new Rectangle();
}
return nullptr; // Unknown type
}
int main() {
Shape* shape = createShape("circle");
if (shape != nullptr) {
shape->draw();
delete shape;
} else {
cout << "Unknown shape" << endl;
}
}
Example 4: With Smart Pointers
#include <memory>
class Resource {
public:
Resource() {
cout << "Resource created" << endl;
}
~Resource() {
cout << "Resource destroyed" << endl;
}
void use() {
cout << "Resource used" << endl;
}
};
int main() {
unique_ptr<Resource> ptr1 = nullptr;
if (ptr1 == nullptr) {
cout << "ptr1 is nullptr" << endl;
}
ptr1 = make_unique<Resource>();
if (ptr1 != nullptr) {
ptr1->use();
}
// Reset to nullptr
ptr1 = nullptr; // Resource destroyed
}
nullptr_t Type
#include <cstddef>
// Type of nullptr
using nullptr_t = decltype(nullptr);
void func(nullptr_t) {
cout << "Received nullptr" << endl;
}
void func(int*) {
cout << "Received pointer" << endl;
}
int main() {
func(nullptr); // nullptr version
int* p = nullptr;
func(p); // pointer version
}
nullptr_t with Templates and Auto Conversion
std::nullptr_t is defined in <cstddef> and is the same as decltype(nullptr). It’s useful when you want to accept “only null pointers” in template arguments or auto deduction.
#include <cstddef>
#include <type_traits>
template<class T>
void reset(T* p) {
delete p;
}
// Separate overload for nullptr_t-specific handling if needed
void take(std::nullptr_t) { /* no-op */ }
void take(int* p) { /* ....*/ }
static_assert(std::is_same<decltype(nullptr), std::nullptr_t>::value, "");
// C++17+: std::is_same_v<decltype(nullptr), std::nullptr_t>
nullptr is also used the same way for member pointers, and the big advantage over 0/NULL is no implicit conversion to integers.
Practical Example: nullptr in API Design
Return Values: While patterns using optional/expected (C++23) are also common, if it’s a raw pointer API, it’s typical to document that nullptr is the only failure value.
// Returns nullptr on lookup failure — caller must always check
const Widget* findWidget(int id) const;
// Default argument: "no options"
void connect(const Options* opts = nullptr);
Smart Pointers: std::unique_ptr<T>/shared_ptr<T> have clear semantics when reset to nullptr, and if (ptr) naturally connects with operator bool.
Migrating Pre-C++11 Code
- Pointer Initialization/Comparison:
= NULL,== NULL,== 0→ nullptr (when the meaning is “pointer null”). - Remove
NULLMacro: To reduce header dependencies, just usenullptrfrom<cstddef>. - Check for Broken Overloads: If
f(NULL)previously calledint, after changing tof(nullptr), check unit tests and call sites. - Boundary with C Code:
NULLmay remain in C compilation units. Gradually unify only the C++ side tonullptr. - Compiler Warnings: Use
-Wzero-as-null-pointer-constant(GCC/Clang) to catch0null constant usage and fix to nullptr.
// Before (C++03 style)
void bar(int* p);
bar(0);
bar(NULL);
// After (C++11+)
void bar(int* p);
bar(nullptr);
Common Problems
Problem 1: Using NULL
// ❌ Still using NULL after C++11
void func(int* ptr) {
if (ptr == NULL) { // Old-fashioned
// ...
}
}
// ✅ Use nullptr
void func(int* ptr) {
if (ptr == nullptr) {
// ...
}
}
Problem 2: Confusion with Integers
// ❌ Using 0 as null pointer
int* ptr = 0;
if (ptr == 0) {
// ...
}
// ✅ Use nullptr
int* ptr = nullptr;
if (ptr == nullptr) {
// ...
}
Problem 3: Overloading Issues
void process(int x) {
cout << "int" << endl;
}
void process(int* ptr) {
cout << "pointer" << endl;
}
// ❌ Using NULL (ambiguous)
process(NULL); // int version called (not intended)
// ✅ Use nullptr
process(nullptr); // pointer version called
nullptr Check Methods
int* ptr = nullptr;
// Method 1: Explicit comparison
if (ptr == nullptr) {
cout << "null" << endl;
}
// Method 2: Implicit conversion
if (!ptr) {
cout << "null" << endl;
}
// Method 3: Logical operation
if (ptr) {
*ptr = 10;
} else {
cout << "null" << endl;
}
NULL vs nullptr Comparison
// NULL (C++03)
#define NULL 0 // or ((void*)0)
int* p1 = NULL;
int x = NULL; // OK (problem!)
// nullptr (C++11)
int* p2 = nullptr;
// int y = nullptr; // Error (type-safe)
// In templates
template<typename T>
void func(T value) {
if (value == nullptr) { // OK
// ...
}
}
func(NULL); // Compile error (NULL is int)
func(nullptr); // OK
Practical Patterns
Pattern 1: Safe Pointer Return
template<typename T>
class Repository {
std::map<int, T> storage_;
public:
// Safe return using nullptr
T* find(int id) {
auto it = storage_.find(id);
if (it != storage_.end()) {
return &it->second;
}
return nullptr; // Not found
}
// Usage
void process(int id) {
T* item = find(id);
if (item != nullptr) {
// Use safely
item->process();
} else {
std::cout << "Item not found\n";
}
}
};
Pattern 2: Optional Callback
class EventHandler {
using Callback = void(*)();
Callback onSuccess_ = nullptr;
Callback onError_ = nullptr;
public:
void setOnSuccess(Callback cb) {
onSuccess_ = cb;
}
void setOnError(Callback cb) {
onError_ = cb;
}
void execute() {
try {
// Perform work
if (onSuccess_ != nullptr) {
onSuccess_();
}
} catch (...) {
if (onError_ != nullptr) {
onError_();
}
}
}
};
// Usage
EventHandler handler;
handler.setOnSuccess([] { std::cout << "Success\n"; });
handler.execute();
Pattern 3: RAII Guard
template<typename T>
class ScopedPtr {
T* ptr_;
public:
explicit ScopedPtr(T* p = nullptr) : ptr_(p) {}
~ScopedPtr() {
if (ptr_ != nullptr) {
delete ptr_;
}
}
// Disable copying
ScopedPtr(const ScopedPtr&) = delete;
ScopedPtr& operator=(const ScopedPtr&) = delete;
// Allow moving
ScopedPtr(ScopedPtr&& other) noexcept : ptr_(other.ptr_) {
other.ptr_ = nullptr;
}
ScopedPtr& operator=(ScopedPtr&& other) noexcept {
if (this != &other) {
if (ptr_ != nullptr) {
delete ptr_;
}
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}
T* get() const { return ptr_; }
T* operator->() const { return ptr_; }
T& operator*() const { return *ptr_; }
explicit operator bool() const {
return ptr_ != nullptr;
}
};
// Usage
ScopedPtr<int> ptr(new int(42));
if (ptr) {
std::cout << *ptr << '\n';
}
FAQ
Q1: nullptr vs NULL?
A:
- nullptr: Type-safe, C++11+, pointer-only
- NULL: Macro (usually
0), type-unsafe, can be interpreted as integer
// NULL's problem
void func(int x) { std::cout << "int\n"; }
void func(int* ptr) { std::cout << "pointer\n"; }
func(NULL); // Outputs "int" (problem!)
func(nullptr); // Outputs "pointer" (as intended)
Recommendation: Always use nullptr in C++11 and later
Q2: nullptr vs 0?
A:
- nullptr: Pointer-only, type-safe
- 0: Both integer and pointer possible, can be confusing
int* p1 = 0; // OK (but confusing)
int* p2 = nullptr; // OK (clear)
int x = 0; // OK
// int y = nullptr; // Error: cannot assign to integer
Q3: What type is nullptr?
A: It’s of type std::nullptr_t. It can be implicitly converted to all pointer types, but cannot be converted to integer types.
#include <cstddef>
std::nullptr_t null = nullptr;
int* p1 = null; // OK
char* p2 = null; // OK
// int x = null; // Error
Q4: Can you assign nullptr to an integer?
A: No. A compile error occurs. This is nullptr’s type safety.
int* ptr = nullptr; // OK
// int x = nullptr; // Error: cannot assign to integer
// Explicit casting also not possible
// int y = static_cast<int>(nullptr); // Error
Q5: When should you use nullptr?
A: Always. In C++11 and later, it’s recommended to use nullptr instead of NULL or 0.
// ❌ Old-fashioned
int* p1 = NULL;
int* p2 = 0;
// ✅ Modern
int* p3 = nullptr;
Q6: Is nullptr evaluated as false?
A: Yes. nullptr is evaluated as false in boolean context.
int* ptr = nullptr;
if (ptr) {
// Not executed
}
if (!ptr) {
std::cout << "null pointer\n"; // Executed
}
// Explicit comparison
if (ptr == nullptr) {
std::cout << "null pointer\n"; // Executed
}
Q7: How do you use nullptr in function overloading?
A: nullptr clearly selects the pointer overload.
void process(int x) {
std::cout << "int: " << x << '\n';
}
void process(int* ptr) {
std::cout << "pointer\n";
}
void process(std::nullptr_t) {
std::cout << "nullptr\n";
}
process(0); // "int: 0"
process(NULL); // "int: 0" (problem!)
process(nullptr); // "nullptr" or "pointer"
int* p = nullptr;
process(p); // "pointer"
Q8: Learning resources for nullptr?
A:
- “Effective Modern C++” (Item 8: Prefer nullptr to 0 and NULL) by Scott Meyers
- cppreference.com - nullptr
- “C++ Primer” (5th Edition) by Stanley Lippman
Related Articles: Pointer Basics, Smart Pointers.
One-line Summary:
nullptris a type-safe C++11 null pointer literal that solves the problems ofNULLand0.