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 비교
| 특징 | nullptr | NULL | 0 |
|---|---|---|---|
| 타입 | std::nullptr_t | int (또는 long) | int |
| 함수 오버로딩 | 포인터 버전 호출 | 정수 버전 호출 | 정수 버전 호출 |
| 템플릿 추론 | std::nullptr_t | int | int |
| 타입 안전성 | ✓ | ✗ | ✗ |
| 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
정리
핵심 요약
- 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로 초기화
다음 단계
- C++ nullptr
- C++ 스마트 포인터
- C++ 포인터 기초
관련 글
- C++ nullptr |
- C++ async & launch |
- C++ Atomic Operations |
- C++ Attributes |
- C++ auto 키워드 |