본문으로 건너뛰기
Previous
Next
C++ nullptr | 'Null Pointer' Guide

C++ nullptr | 'Null Pointer' Guide

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

Here is the func implementation:

#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

Here is the func implementation:

// 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

Here is the func implementation:

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

Here is the append implementation:

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

Here is the log implementation:

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

Here is the draw implementation:

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

Here is the use implementation:

#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

Here is the func implementation:

#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

Here is the func implementation:

// ❌ 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

The following example demonstrates the concept in cpp:

// ❌ Using 0 as null pointer
int* ptr = 0;

if (ptr == 0) {
    // ...
}

// ✅ Use nullptr
int* ptr = nullptr;

if (ptr == nullptr) {
    // ...
}

Problem 3: Overloading Issues

Here is the process implementation:

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

The following example demonstrates the concept in cpp:

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

Here is the func implementation:

// 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

Here is the process implementation:

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

Here is the setOnSuccess implementation:

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

Here is the ScopedPtr implementation:

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.

The following example demonstrates the concept in cpp:

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.

Here is the process implementation:

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.



같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, nullptr, pointer, NULL, C++11 등으로 검색하시면 이 글이 도움이 됩니다.