C++ nullptr vs NULL | "널 포인터" 가이드

C++ nullptr vs NULL | "널 포인터" 가이드

이 글의 핵심

C++ nullptr vs NULL에 대한 실전 가이드입니다.

들어가며

C++11 nullptr은 타입 안전한 널 포인터 리터럴입니다. 기존의 NULL이나 0과 달리 std::nullptr_t 타입을 가지며, 함수 오버로딩과 템플릿에서 명확한 의미를 제공합니다.


1. nullptr 기본

nullptr이란?

#include <iostream>

int main() {
    // C++03 이전
    int* ptr1 = NULL;   // 0 또는 ((void*)0)
    int* ptr2 = 0;      // 정수 0
    
    // C++11 이후
    int* ptr3 = nullptr;  // std::nullptr_t
    
    // 모두 널 포인터지만 타입이 다름
    std::cout << "ptr1: " << ptr1 << std::endl;  // 0
    std::cout << "ptr2: " << ptr2 << std::endl;  // 0
    std::cout << "ptr3: " << ptr3 << std::endl;  // 0
    
    return 0;
}

nullptr_t 타입

#include <iostream>
#include <cstddef>

void func(std::nullptr_t) {
    std::cout << "nullptr_t" << std::endl;
}

void func(int) {
    std::cout << "int" << std::endl;
}

int main() {
    func(nullptr);  // "nullptr_t"
    // func(NULL);  // 컴파일 에러 (모호함)
    // func(0);     // "int"
    
    return 0;
}

2. NULL의 문제점

문제 1: 함수 오버로딩

#include <iostream>

void process(int value) {
    std::cout << "정수: " << value << std::endl;
}

void process(int* ptr) {
    std::cout << "포인터" << std::endl;
}

int main() {
    process(0);        // "정수: 0"
    process(NULL);     // "정수: 0" (의도하지 않음!)
    process(nullptr);  // "포인터" (올바름)
    
    return 0;
}

문제: NULL은 정수 0으로 정의되어 process(int)가 호출됩니다.

문제 2: 템플릿 타입 추론

#include <iostream>

template<typename T>
void func(T value) {
    std::cout << "T의 타입: " << typeid(T).name() << std::endl;
}

int main() {
    func(0);        // T = int
    func(NULL);     // T = int (또는 long)
    func(nullptr);  // T = std::nullptr_t
    
    return 0;
}

문제: NULL은 정수로 추론되어 포인터가 아닙니다.

문제 3: auto 타입 추론

#include <iostream>

int main() {
    auto p1 = NULL;     // int (또는 long)
    auto p2 = nullptr;  // std::nullptr_t
    
    // ❌ p1은 정수
    // int* ptr1 = p1;  // 경고 또는 에러
    
    // ✅ p2는 포인터
    int* ptr2 = p2;  // OK
    
    std::cout << "p1 타입: " << typeid(p1).name() << std::endl;
    std::cout << "p2 타입: " << typeid(p2).name() << std::endl;
    
    return 0;
}

3. nullptr 사용법

포인터 초기화

#include <iostream>

int main() {
    // ✅ nullptr 사용
    int* ptr1 = nullptr;
    char* ptr2 = nullptr;
    double* ptr3 = nullptr;
    
    // ✅ 스마트 포인터
    std::unique_ptr<int> uptr = nullptr;
    std::shared_ptr<int> sptr = nullptr;
    
    // ✅ 함수 포인터
    void (*funcPtr)() = nullptr;
    
    return 0;
}

포인터 체크

#include <iostream>

void process(int* ptr) {
    // ✅ nullptr 비교 (명확함)
    if (ptr == nullptr) {
        std::cout << "널 포인터" << std::endl;
        return;
    }
    
    // ✅ 간단한 체크 (관용적)
    if (!ptr) {
        std::cout << "널 포인터" << std::endl;
        return;
    }
    
    // ✅ 역으로 체크
    if (ptr) {
        std::cout << "유효한 포인터: " << *ptr << std::endl;
    }
}

int main() {
    int value = 42;
    int* ptr = &value;
    
    process(ptr);
    process(nullptr);
    
    return 0;
}

함수 반환

#include <iostream>

// ❌ 0 반환 (명확하지 않음)
int* findValue1(int target) {
    // ...
    return 0;  // 가능하지만 의도가 불명확
}

// ✅ nullptr 반환 (명확함)
int* findValue2(int target) {
    // ...
    return nullptr;  // 명확하게 널 포인터 반환
}

int main() {
    int* result = findValue2(42);
    
    if (result == nullptr) {
        std::cout << "찾지 못함" << std::endl;
    }
    
    return 0;
}

4. 실전 예제

예제 1: 연결 리스트

#include <iostream>
#include <memory>

struct Node {
    int data;
    Node* next;
    
    Node(int d) : data(d), next(nullptr) {}
};

class LinkedList {
    Node* head;
    
public:
    LinkedList() : head(nullptr) {}
    
    ~LinkedList() {
        while (head != nullptr) {
            Node* temp = head;
            head = head->next;
            delete temp;
        }
    }
    
    void push(int data) {
        Node* newNode = new Node(data);
        newNode->next = head;
        head = newNode;
    }
    
    void print() const {
        Node* current = head;
        while (current != nullptr) {
            std::cout << current->data << " -> ";
            current = current->next;
        }
        std::cout << "nullptr" << std::endl;
    }
};

int main() {
    LinkedList list;
    list.push(3);
    list.push(2);
    list.push(1);
    
    list.print();  // 1 -> 2 -> 3 -> nullptr
    
    return 0;
}

예제 2: 옵셔널 포인터

#include <iostream>
#include <string>

class User {
public:
    std::string name;
    int age;
    
    User(const std::string& n, int a) : name(n), age(a) {}
};

class UserRepository {
public:
    // 찾지 못하면 nullptr 반환
    User* findById(int id) {
        if (id == 1) {
            static User user("홍길동", 25);
            return &user;
        }
        return nullptr;
    }
};

int main() {
    UserRepository repo;
    
    User* user = repo.findById(1);
    if (user != nullptr) {
        std::cout << "찾음: " << user->name << std::endl;
    } else {
        std::cout << "찾지 못함" << std::endl;
    }
    
    User* notFound = repo.findById(999);
    if (notFound == nullptr) {
        std::cout << "사용자 없음" << std::endl;
    }
    
    return 0;
}

5. nullptr vs NULL vs 0 비교

특징nullptrNULL0
타입std::nullptr_tint (또는 long)int
함수 오버로딩포인터 버전 호출정수 버전 호출정수 버전 호출
템플릿 추론std::nullptr_tintint
타입 안전성
C++ 버전C++11+모든 버전모든 버전
권장 사용

6. 마이그레이션 가이드

기존 코드

// C++03 스타일
int* ptr = NULL;

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

void func(Widget* w = NULL) {
    // ...
}

C++11 스타일

// C++11 스타일
int* ptr = nullptr;

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

// 또는 간결하게
if (!ptr) {
    // ...
}

void func(Widget* w = nullptr) {
    // ...
}

자동 변환 (Clang-Tidy)

# Clang-Tidy로 자동 변환
clang-tidy -checks='-*,modernize-use-nullptr' -fix program.cpp

정리

핵심 요약

  1. nullptr: C++11 타입 안전 널 포인터
  2. NULL: 정수 0, 레거시
  3. 타입: std::nullptr_t vs int
  4. 오버로딩: nullptr은 포인터 버전 호출
  5. 템플릿: nullptr은 타입 추론 명확
  6. 성능: 차이 없음 (컴파일 타임)

nullptr vs NULL

상황nullptrNULL
함수 오버로딩포인터 버전 호출정수 버전 호출 (문제)
템플릿 타입 추론std::nullptr_tint (문제)
auto 추론std::nullptr_tint (문제)
가독성명확 (포인터)모호 (정수?)
타입 안전성높음낮음

실전 팁

사용 원칙:

  • C++11 이후에는 항상 nullptr 사용
  • NULL0은 레거시 코드에서만
  • 포인터 비교는 ptr == nullptr 또는 !ptr

마이그레이션:

  • 기존 코드의 NULLnullptr로 교체
  • Clang-Tidy 자동 변환 도구 활용
  • 컴파일러 경고 활성화 (-Wzero-as-null-pointer-constant)

주의사항:

  • 소멸자에서 포인터를 nullptr로 설정할 필요 없음 (객체 소멸됨)
  • delete nullptr은 안전 (아무 일도 안 함)
  • 스마트 포인터도 nullptr로 초기화

다음 단계

  • C++ nullptr
  • C++ 스마트 포인터
  • C++ 포인터 기초

관련 글

  • C++ nullptr |
  • C++ async & launch |
  • C++ Atomic Operations |
  • C++ Attributes |
  • C++ auto 키워드 |