<개인공부>/[C++]

[C++] nullptr (널 포인터 리터럴)

BlockDMask 2021. 2. 17. 00:30
반응형

안녕하세요. BlockDMask입니다.
오늘은 C++11에서 도입된 새로운 키워드 nullptr에 대해서 이야기해보려 합니다.

기존 C언어, C++을 사용하시는 많은 분들이 0, NULL 이렇게 널을 사용하셨을 텐데요.
C++11부터는 nullptr을 사용하시면 좋을 것 같습니다.

<목차>
1. C++ nullptr 이란?
2. C++ NULL, nullptr의 차이점

 

 

1. C++11 nullptr 설명


모든 변수에는 초기화하는 방법이 있습니다.
기존 C, C++에서는 포인터를 초기화할 때 0을 이용해서 초기화를 하였는데요.
C++11부터는 nullptr을 가지고 초기화를 하시면 됩니다.

1-1) nullptr

nullptr을 한 줄로 말하자면 널 포인터 값(null pointer value)을 나타내는 포인터 리터럴(pointer literal)이라 하는데
포인터를 표현하는 값 중에 "널을 표현한 값"이다.라고 할 수 있습니다.

int* ptr1 = 0;
int* ptr2 = nullptr;

포인터 변수를 초기화하기 위해 기존의 방식대로 0을 사용해도 되지만, nullptr을 사용하는 것이 안전하고 코드의 가독성을 높일 수 있다.

기존 포인터를 초기화할 때는 p1처럼 0 혹은 #define NULL 0을 이용해서 NULL 이라든지 이렇게 진행을 하셨을 텐데, 앞으로는 nullptr로 초기화가 가능해집니다.

이전에 0을 이용해서 포인터를 초기화하거나, 포인터의 널 체크를 0으로 했을 때, 모호한 상황들이 있었을 것입니다. 하지만 nullptr을 이용하면 모호한 상황이 아닌 포인터만 딱 체크를 할 수 있다는 장점 이 있습니다.

 

 

1-2) std::nullptr_t 타입

C++11부터 만들어진 nullptr의 타입은 <cstddef> 헤더에 있는 std::nullptr_t 타입입니다.

0이 int 타입이고, 0.0이 double타입, 0.0f가 float 타입이듯이 모든 변수에는 데이터 타입이 존재합니다.
nullptr의 데이터 타입은 std::nullptr_t라는 타입입니다.

해당 std::nullptr_t 타입은 모든 타입의 포인터로 암시적 형 변환이 가능합니다.

예를 들어 아래와 같이 모든 타입의 포인터를 초기화할 수 있습니다.

1
2
3
4
5
6
int* ptr1 = nullptr;                    // int 포인터
char* ptr2 = nullptr;                   // char 포인터
double* ptr3 = nullptr;                 // double 포인터
void (*func1) (int a, int b) = nullptr; // 함수 포인터1
void (*func2) () = nullptr;             // 함수 포인터2
 
cs

 

 

1-3) nullptr 장점 : 가독성과 코드 안정성

nullptr을 사용하면 가독성이 좋아집니다.
이전에 이야기했듯이 기존에 우리가 널을 체크할 때는 0을 사용했었습니다.
하지만 C++이후 nullptr을 사용했을 때 널을 체크하는 것이 뚜렷해집니다. 아래 예제를 한번 보시죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(void) {
    int* a = 0;  //포인터
 
 
    // a가 int 값의 0인지 포인터 타입인지 헷갈릴 수 있음.
    if (a == 0)
    {
        cout << "a == 0" << endl;
    }
 
    // a가 포인터 타입인지 알 수 있음.
    if (a == nullptr)
    {
        cout << "a == nullptr" << endl;
    }
 
    return 0;
}
 
cs

if (a == 0) 부분을 보면 "변수 a가 int 타입인가? 0을 체크하는 것을 보면 int 일수도 있겠다" 싶어서 혹시 몰라 선언 부분으로 가면 int* 타입인 것을 볼 수 있습니다. 그러므로 "0이 숫자 0이 아니라 널 체크를 위한 0이구나"라는 흐름으로 코드를 읽을 수도 있습니다.

만약에 if( a == nullptr)로 되어있다면, 이것은 누가 봐도 "a가 포인터 타입이구나"라고 바로 알 수 있습니다.
그렇기 때문에 nullptr을 사용하게 되면 코드 가독성이 높아진다고 하는 것이고, 포인터의 타입을 0과 섞어 쓰지 않아도 되니, nullptr을 사용하게 되면 좀 더 정확하게 포인터 타입을 초기화하고, 다룰 수 있게 됩니다. 그래서 코드의 안정성이 좋아지죠. 안정성 관련해서는 아래 NULL, nullptr의 차이점을 보셔도 이해가 갈 것 입니다. 함수호출을 잘 보시면 됩니다.

 

 

2. NULL, nullptr 차이점


컴파일러의 종류나 버전에 따라 다르지만, 제 비주얼 스튜디오 2017 기준으로 보자면 C++의 NULL은 아래와 같이 정의되어있습니다.

C++ NULL 매크로

이것을 풀어 말씀드리면
C++에서는 NULL을 0으로 사용하고
C++ 이 아닌 C에서는 NULL을 ((void*) 0)으로 치환해서 사용하겠다는 것입니다.

NULL은 진짜 널을 가리키는 포인터가 아니라 숫자 0이었다는 것이었습니다.
(널 문자 '\0'를 가리키는 숫자입니다.)
그러니 가짜를 쓰지 말고 진짜 포인터를 표현할 수 있는 nullptr을 사용해야 합니다.

예제에서 NULL과 nullptr이 무엇이 같고 다른지 한번 확인해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<iostream>
using namespace std;
 
void func(int a) { cout << "int 호출" << endl; }
void func(int* b) { cout << "int* 호출" << endl; }
 
int main(void) {
    // NULL, nullptr 비교1
    cout << endl << "== NULL, nullptr 비교1" << endl;
    func(0);        // int 호출
    func(NULL);     // int 호출
    func((int*)0);  // int* 호출
   func(nullptr);  // int* 호출
 
 
    // NULL, nullptr 비교2
    cout << endl << "== NULL, nullptr 비교2" << endl;
    int* ptr1 = NULL;
    int* ptr2 = nullptr;
 
    if (ptr1 == NULL) { cout << "2-1. NULL == NULL" << endl; }
    if (ptr2 == NULL) { cout << "2-2. nullptr == NULL" << endl; }
    if (ptr1 == nullptr) { cout << "2-3. NULL == nullptr" << endl; }
    if (ptr2 == nullptr) { cout << "2-4. nullptr == nullptr" << endl; }
    if (ptr1 == ptr2) { cout << "2-5. NULL == nullptr" << endl; }
 
 
    // NULL, nullptr 비교3
    cout << endl << "== NULL, nullptr 비교3" << endl;
    int a = 0
    if (a == NULL) { cout << "3-1. int 타입 0 == NULL" << endl; }
    // if (a == nullptr) { cout << "3-2. int 타입 0 == nullptr" << endl; } ERROR
 
    return 0;
}
 
cs

C++ NULL, nullptr 비교

- 예제 1
인자로 0,  NULL을 넣게 되면 0 리터럴이 들어간 것과 같기 때문에 func(int a) 함수가 호출되는 것을 볼 수 있고,
인자로 ((int*) 0),  nullptr을 넣게 되면 func(int* a)가 호출되는 것을 볼 수 있습니다.

여기서 알 수 있듯, NULL은 0을 nullptr은 포인터를 뜻한다는 것을 알 수 있습니다.

**NULL이 좀 모호하죠? 널을 인자로 보내놨더니 int 타입 숫자 0으로 인식되어서 func(int a)를 호출하니 모호하기 짝이없습니다.)

- 예제 2
하지만 예제 2번을 보시면 NULL과 nullptr을 비교해보면, 완전히 다르다 라고는 할 수 없습니다.
NULL 이 0 리터럴 이긴 하지만, nullptr과 비교 == 를 하면 true가 나오는 것을 볼 수 있습니다.

이유는 예제 2번의 0은 int의 0이 아니라 int* 의 0 이므로 널을 가리키는 게 맞기 때문에 nullptr과 비교가 가능해진 것 이기 때문입니다.

- 예제 3
하지만, int 타입의 0과 NULL, nullptr을 비교해 보면 어떨까요?

결과를 보면 int타입의 숫자 0과 NULL은 비교가 가능하고,
int 타입의 숫자 0과 nullptr은 아예 비교 자체가 불가능 ERROR 가 나옵니다.

이걸 통해 알 수 있는 것은 NULL은 그냥 숫자 0이기 때문에 어떨 때는 int 타입이랑 비교가 되고, int 타입의 인자로 들어가서 함수를 호출하기도 하고 어떨때는 포인터 0으로 널을 가리키고 아주 애매모호한 녀석이라는 것을 알 수 있습니다.

이런 양다리 걸치는 NULL을 사용하지 말고, 순수하게 포인터를 가리키는 nullptr을 사용하는 걸 추천드립니다.

 

결론 : 포인터 변수를 초기화하거나 값을 조사할 때는 0 또는 NULL을 사용하지 말고 nullptr을 사용하자.

이상으로 오늘은 기존 NULL과 C++11 nullptr에 대해서 알아보았습니다.
오늘도 방문해주셔서 감사합니다.

반응형