본문 바로가기
  • 쓸쓸한 개발자의 공부방
C++

[C++] 동적메모리 할당 (new, delete), nullptr

by 심찬 2021. 8. 15.

 

 

new / delete

 

C언어에서 메모리 할당할 때 malloc을 사용한다. 해제할 때는 free를 사용한다. C언어에서는 malloc사용시 캐스팅이 필요없다.

C++에서 malloc 사용 가능하다. 그러나 반환되는 주소는 원하는 포인터 타입으로 캐스팅 해야 한다.

#include <cstdlib>

int main()
{
    int* p1 = (int*)malloc(sizeof(int)*10);
             // malloc 사용시 c에서는 (int*) 캐스팅 없이 사용가능하지만
             // c++에서는 (int*)와 같은 캐스팅이 필요하다.
    
    free(p1);
}

 

C언어에서의 동적 메모리 할당

- new로 할당하고 delete로 해지한다.

- new가 반환한 주소를 캐스팅 할 필요가 없다.

- 배열 형태로 할당한 경우 delete[]로 해지 해야 한다.

 

malloc은 메모리 공간만을 만들어 준다면 (생성자 미호출), new는 메모리 공간을 만들면서 생성자를 호출한다.

#include <cstdlib>  

int main()
{
    int* p2 = new int; // int 1 개, 4byte 할당
    delete p2;
    
    
    int* p3 = new int[10];
    delete[] p3;            // 배열 지울 때는 [ ] 넣어줘야 함.
    
    
    int* p4 = new int[10][2]; // int 20개 * 4 byte = 80 byte
    delete[] p4;             // 2차원 배열 지울 때도 [ ] 하나만 넣어주면 된다.
    
    
    int* p5 = new int[10];
    delete p5; // 불가! undefined : 컴파일러마다 동작 방식이 다름.
    

}

 

 

nullptr

 

 - c++11 부터 지원하는 새로운 키워드

 - 0을 사용해도 되지만, nullptr은 가독성을 높이고 더 안전하다.

int main()
{
    int* p1 = 0;
    int* p2 = nullptr;
}

 

 

nullptr : pointer literal

 

리터럴?

 - 리터럴은 소스 코드 내에서 사용되는 고정된 값이다.

 - 변수 초기화, 구문 등에서 많이 사용된다.

 - 모든 리터널을 데이터 타입이 있다.

 

 

0은 정수, 실수, bool,  포인터 등의 변수 초기화 할때 사용 가능하다.

int main()
{
    int    n = 0;
    double d = 0; 
    bool   b = 0;
    char*  s = 0;
}

 

해당 예제에서 foo(0); 를 호출하면 0 값을 int로 판단하여 foo (int n)이 호출된다!

#include <iostream>

void foo(int    n) { std::cout << "int"    << std::endl; }
void foo(double d) { std::cout << "double" << std::endl; }
void foo(bool   b) { std::cout << "bool"   << std::endl; }
void foo(char*  s) { std::cout << "char*"  << std::endl; }

int main()
{
    int    n = 0;
    
    foo(0); // int 로 인식된다.
}

 

 

아래와 같이 foo(int n) 함수가 없을 때, foo(0); 를 호출하면 double, bool, char* 중 어느 것을 호출해야 할지 모호해서 에러가 발생한다. 

#include <iostream>

//void foo(int    n) { std::cout << "int"    << std::endl; }
void foo(double d) { std::cout << "double" << std::endl; }
void foo(bool   b) { std::cout << "bool"   << std::endl; }
void foo(char*  s) { std::cout << "char*"  << std::endl; }

int main()
{
    foo(0); // error
}

main.cpp:10:10: error: call of overloaded ‘foo(int)’ is ambiguous

그런데 만약 foo 함수의 double, bool, char* 인자를 받는 함수들이 모두 있는게 아닌 하나의 함수가 존재한다면, 정상적으로 호출되는 것을 알 수 있다. 모호하지 않고 단 하나를 선택할 수 있기 때문이다.

#include <iostream>

//void foo(int    n) { std::cout << "int"    << std::endl; }
//void foo(double d) { std::cout << "double" << std::endl; }
void foo(bool   b) { std::cout << "bool"   << std::endl; }
//void foo(char*  s) { std::cout << "char*"  << std::endl; }

int main()
{
    foo(0); // bool
}

 

명확하게 실수 (0.0) 을 호출할 때는 double형이 실행됨을 확인할 수 있다.

#include <iostream>

void foo(int    n) { std::cout << "int"    << std::endl; }
void foo(double d) { std::cout << "double" << std::endl; }
void foo(bool   b) { std::cout << "bool"   << std::endl; }
void foo(char*  s) { std::cout << "char*"  << std::endl; }

int main()
{
    foo(0.0); // 실수 리터럴
}

 

char*를 호출하고자 할때는 (char*)0 와 같이 캐스팅을 통한 호출을 해야했다.

nullptr은 포인터 리터럴로 활용할 수 있고 char*가 호출됨을 아래와 같이 확인할 수 있다.

#include <iostream>

//void foo(int    n) { std::cout << "int"    << std::endl; }
void foo(double d) { std::cout << "double" << std::endl; }
void foo(bool   b) { std::cout << "bool"   << std::endl; }
void foo(char*  s) { std::cout << "char*"  << std::endl; }

int main()
{
    //foo(0); // int 로 인식된다.
    //foo(0.0); // 실수 리터럴
    //foo(false);  // bool
    //foo( (char*)0 ); // char*, 포인터 리터럴이 필요했는데...
    foo( nullptr ); // char*, 포인터 리터럴로 nullptr을 활용할 수 있다.
}

 

ret 값이 auto로 되어 있어 코드를 봤을 때 if ( ret==0) 가 정확하게 무엇과의 비교인지 알기가 어렵다.

가독성을 위해 nullptr로 비교하는 것을 추천한다.

int* foo() { return 0; }


int main()
{
    auto ret = foo();
    
    //if ( ret == 0 )    // 0 을 사용하면 이게 int인지 int* 인지... 알기가 어렵다. (가독성 저하!)
    
    if ( ret == nullptr )  // 이처럼 명확하게 nullptr을 사용하기를 추천한다.!!!
    {
        //...
    }    
}

 

nullptr을 사용할 수 있는 경우가 한정되어 있다.

 - 포인터 형태에는 nullptr이 초기화가 가능하다.

 - int 에는 사용이 불가하며 0로 초기화가 가능하다.

int main()
{
//    0;      // int
//    0.0;    // double
//    nullptr;// std::nullptr_t
    
    int*  p1 = nullptr;
    char* p2 = nullptr;
    void(*f)() = nullptr;
    
    int n1 = nullptr; // error
    int n2 = 0;
    
    bool b1 = nullptr; // error : 복사 초기화는 불가
    bool b2(nullptr); // ok
    bool b3{nullptr}; // ok   직접 초기화는 가능
    bool b4 = {nullptr}; // error
}

main.cpp:11:14: error: cannot convert ‘std::nullptr_t’ to ‘int’ in initialization

main.cpp:14:15: error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]

main.cpp:17:23: error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]

 

NULL과 nullptr의 차이?

 

NULL과 nullptr은 다르다. c++에서는 nullptr를 사용하기를 권장한다.

NULL이 c++에서는 0으로 취급된다.

 

C에서는 void* -> 다른 타입으로의 암시적 변환 허용O

C++에서는 void* -> 다른 타입으로의 암시적 변환 허용X

#include <iostream>

void foo(int   n) { std::cout << "int"   << std::endl; }
void foo(void* p) { std::cout << "void*" << std::endl; }
void goo(char* n) { std::cout << "goo" << std::endl; }

// c++컴파일러에 대부분 이러한 방식으로 되어 있다.
/*
#ifdef __cplusplus
    #define NULL 0
#else
    #define NULL (void*)0
#endif
*/

int main()
{
    foo(0); // int
    foo((void*)0); // void*    : c++11 이전에 사용법
    foo(NULL); // int ?
    
    goo(NULL); //  ok
}

NULL이 int 로 취급이 되기 때문에 의심없이 foo(int)로 호출될 것이라 생각했는데,,, 모호하다고 error가 나온다!

 

 

 

 

'C++' 카테고리의 다른 글

[C++] STL : stack, vector  (0) 2021.08.17
[C++] 객체 지향 프로그래밍 OOP  (0) 2021.08.16
[C++] Explicit Casting (명시적 캐스팅)  (0) 2021.08.14
[C++] const reference, return by reference  (0) 2021.08.14
[C++] reference 개념  (0) 2021.08.13

댓글