[2026] C++ nullptr | Null Pointer Guide

[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.

ExpressionMeaningSelects f(int)Pointer IntentAssign to int variable
0Integer 0 or null pointer constantPossibleConverts to pointer depending on contextint x = 0 OK
NULLImplementation-defined (0, 0L, etc.)Often integer sideMay differ from intentPossible depending on implementation (warning)
nullptrstd::nullptr_tGenerally NoMatches pointer/nullptr_t overloadImpossible (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

  1. Pointer Initialization/Comparison: = NULL, == NULL, == 0nullptr (when the meaning is “pointer null”).
  2. Remove NULL Macro: To reduce header dependencies, just use nullptr from <cstddef>.
  3. Check for Broken Overloads: If f(NULL) previously called int, after changing to f(nullptr), check unit tests and call sites.
  4. Boundary with C Code: NULL may remain in C compilation units. Gradually unify only the C++ side to nullptr.
  5. Compiler Warnings: Use -Wzero-as-null-pointer-constant (GCC/Clang) to catch 0 null 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: nullptr is a type-safe C++11 null pointer literal that solves the problems of NULL and 0.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3