[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 비교
| 특징 | nullptr | NULL | 0 |
|---|---|---|---|
| 타입 | std::nullptr_t | int (또는 long) | int |
| 함수 오버로딩 | 포인터 버전 호출 | 정수 버전 호출 | 정수 버전 호출 |
| 템플릿 추론 | std::nullptr_t | int | int |
| 타입 안전성 | ✓ | ✗ | ✗ |
| 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
정리
핵심 요약
- nullptr: C++11 타입 안전 널 포인터
- NULL: 정수 0, 레거시
- 타입:
std::nullptr_tvsint - 오버로딩: nullptr은 포인터 버전 호출
- 템플릿: nullptr은 타입 추론 명확
- 성능: 차이 없음 (컴파일 타임)
nullptr vs NULL
| 상황 | nullptr | NULL |
|---|---|---|
| 함수 오버로딩 | 포인터 버전 호출 | 정수 버전 호출 (문제) |
| 템플릿 타입 추론 | std::nullptr_t | int (문제) |
| auto 추론 | std::nullptr_t | int (문제) |
| 가독성 | 명확 (포인터) | 모호 (정수?) |
| 타입 안전성 | 높음 | 낮음 |
실전 팁
사용 원칙:
- C++11 이후에는 항상
nullptr사용 NULL과0은 레거시 코드에서만- 포인터 비교는
ptr == nullptr또는!ptr마이그레이션: - 기존 코드의
NULL을nullptr로 교체 - Clang-Tidy 자동 변환 도구 활용
- 컴파일러 경고 활성화 (
-Wzero-as-null-pointer-constant) 주의사항: - 소멸자에서 포인터를
nullptr로 설정할 필요 없음 (객체 소멸됨) delete nullptr은 안전 (아무 일도 안 함)- 스마트 포인터도
nullptr로 초기화