[2026] C++ nullptr vs NULL | 널 포인터 가이드

[2026] C++ nullptr vs NULL | 널 포인터 가이드

이 글의 핵심

C++ nullptr vs NULL: 널 포인터 가이드. nullptr 기본·NULL의 문제점.

들어가며

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

실무에서 마주한 현실

개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.

1. nullptr 기본

nullptr이란?

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 타입

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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: 함수 오버로딩

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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: 템플릿 타입 추론

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 타입 추론

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 사용법

포인터 초기화

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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;
}

포인터 체크

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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;
}

함수 반환

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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: 연결 리스트

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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: 옵셔널 포인터

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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. 마이그레이션 가이드

기존 코드

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// C++03 스타일
int* ptr = NULL;
if (ptr == NULL) {
    // ...
}
void func(Widget* w = NULL) {
    // ...
}

C++11 스타일

다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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로 초기화

다음 단계


관련 글

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