new와 delete에 대한 얘기

2021. 11. 20. 23:20c,c++ 기본/next step

320x100

안녕하세요 대학생 개발자입니다.

 

이번에 다뤄볼 내용은 동적할당에 대한 얘기입니다.

 

할당에는 두가지 방법이 있을겁니다. 정적할당과 동적할당 이렇게 말이죠.

일반적으로 우리가 처음 C++을 배울때 main함수를 사용하게 될텐데요.

메인함수와 같은 함수 즉 지역내에 선언되는 변수들은 모두 정적할당을 통해서 얻어지는 메모리입니다.

컴파일러가 컴파일 타임에 어느정도를 할당해야될지 정해서 올려버리는 값들이죠.

마찬가지로 전역변수도 같습니다. 컴파일타임에 존재하는 변수이기 때문에 정적으로 공간이 할당되어 버리죠...

 

하지만 분명히 우리가 예상하지 않았던 크기의 공간이 필요하게 될 수도있습니다.

물론 스택으로 일부 커버를 할 수 있긴 합니다. 하지만... 100바이트면 충분할줄 알았던 것이 1000바이트가 되면요..?

그럴 경우에는 100바이트르 10번 돌려야되는걸까요? 사실 아주 기본적으로만 본다면 이런 이유에서도 동적할당이 필요하개 되겠죠... 그런다고 1000이 필요하다고해서 매번 1000을 잡아버리면 1, 10,50 이런 작은값이 들어왔을땐 메모리 낭비니까요... 물론 퍼포먼스의 차이는 없습니다만... 메모리 낭비가 있기 때문이죠

 

그때 동적할당을 쓴다고 일반적으로 배우게되죠 그러면 우리는 동적할당을 어떤식으로 써야하며 new delete는 어떤식으로 되어있는 것일까에 대한 얘기를 해보려고 합니다.

 

primitive type의 경우에는 new는 malloc을 호출하고 malloc에서 할당이 불가능하면 std::bad_alloc을 throw 하는 일밖에 하지 않습니다. 즉 malloc에 대한 래핑이죠.

delete의 경우에도 거의 동일합니다. free에 대한 래핑일 뿐이죠 대신  nullptr에 대한 처리를 해준다는점이 다른거고...

그리고 delete에 파라미터로 들어간 포인터의 값을 0x8123으로 바꿔버린다 정도 다른것 같습니다... 뭐 하지만 그렇다고해서 우리가 0x8123을 확인해서 뭔가 하는 로직은 없을겁니다. 그냥 지워진 메모리에 접근을 못하게 하려는 1차원적인 방안이죠 다른 포인터를 이용해서 같은곳을 참조하고 있었다면 여전히 접근가능하게 될겁니다..

 

사실 new와 delete의 경우에는 c++의 객체를 초기화 시키기 위한 기능입니다. 왜냐면 malloc,free만 해도 공간할당은 가능하거든요 지우는것도 가능하구요... 하지만 클래스의 경우에는 생성자와 소멸자라는게 존재하기 때문에 생성자와 소멸자를 호출해주는 역할까지 해주는 기능이 바로 new, delete인것 입니다.

 

그래서 우리가 new delete를 오버로딩 할때도 메모리 할당에 대한 부분만 조절이 가능하지 생성자 소멸자를 호출하는 부분은 건드릴 수 가없는거에요...

 

네 뭐 말씀 드렸던것 처럼 new의 경우에는 그저 malloc의 래핑입니다.

 

delete의 경우에는 반대로 free로 점프해버리는걸 알수있죠.

 

아까 그래서 제가 생성자와 소멸자가 존재하지 않는 클래스에서는 new와 malloc은 동일하고 free와 delete는 동일하다고 말씀드렸던거에요.

 

 

사실 이건 많은사람들이 알고있는 부분이구요...

지금부터 약간 생각안해봤을법한 내용을 다뤄보려고 하는데요...

우리가 new, malloc등으로 공간할당을 요구하면 몇바이트를 할당받을지 직접 기입을 하게됩니다.

그러면 그 바이트 수에 맞춰서 공간을 만들어주고 그걸 리턴해주는데요.(일단은)

 

반환할때는요??.. 분명 우리는 몇바이트를 반환하라는 명시적인 어떤 행위도 하지 않았는데 저절로 반환이 됩니다.

이게 바로 힙이 하고있는 행동입니다. 음... 예전 글에서 아마 힙과 스택은 메모리를 관리하는 도구이지 메모리 그 자체가 아니라는 말씀을 드렸었어요... 힙에서 바로 이런 행동을 대신 해주고있는겁니다. 어디에 몇바이트 할당받았더라 라는 내용을 가지고 있는겁니다.

 

class Test
{
public:
    int a;
    int b;
    int c;
    int d;
};

int main()
{
    Test* pt = new Test[10];

    pt->a = 10;
    pt->b = 20;

    delete[] pt;
}

음... 여기와 같이 보시면 fdfdfdfd의 경우에는 내가 할당 받은 공간 이전과 이후를 알려주는 거구요...

cdcdcdcd의 경우에는 동적할당으로 할당해 놓고 사용하지 않은 부분에 대한 값입니다.

그위에 좀 올려보니 분명 new로 할당받은 곳 위 어딘가에 a0 즉 16바이트 10개의 값이 적혀있다는걸 알 수 있었습니다.

혹시 우연의 일치일수도 있으니 값을 바꿔서 다시 한번 해보도록 하겠습니다.

이번엔 new Test[20]; 으로 바꿔 봤습니다.

네 0x140 즉 320이네요 Test가 16바이트고 20개니까 320바이트 정확히 맞습니다... 이런식으로 힙에서 관리를 해주고 있었다는 겁니다.

 

음... 그럼 여기서 저 40 값을 수정해버리면 어떻게 될지 너무 궁금하지 않으세요??

aa으로 바꿔봤습니다 어떤결과가 일어날지 정말 궁금하군요

엌ㅋㅋㅋ 힙이 터져버렸습니다... 여러분 힙을 쓰실때는 꼭 인덱스에 맞게 쓰셔야합니다. 맞게 쓰지 않으시면 저처럼 이렇게 됬을때 왜 이렇게 됬는지 확인도 못하는 경우가 생깁니다.

 

네 그럼 장난은 여기까지 해보고 갑자기 궁금한 내용이 또 생겨버렸습니다.

아까 말했듯 힙에서 320이라는 값을 가지고 있었습니다.

그러면 c++에서 new의 경우 생성자를 호출하는 기능이 포함되어 있습니다.

그럼 생성자를 new하는 기능은 도대체 어디에 저장한다는 말입니까?

분명 힙이 관리하는 공간에 적을 수는 없을겁니다... 감히 상위 언어주제에 os에서 관리하는 힙에 몰래 숨어들려고 하진 않겠죠.

class Test
{
public:
    Test()
    {
        printf("hello world\n");
    }
    ~Test()
    {
        printf("goodbye world\n");
    }
    int a;
    int b;
    int c;
    int d;
};

int main()
{
    Test* pt = new Test[20];

    pt->a = 10;
    pt->b = 20;

    delete[] pt;
}

클래스를 조금 수정해 봤습니다. 생성자와 소멸자가 존재하는 클래스입니다.

너무나도 당연하겠지만 hello world와 goodbye world가 20번씩 출력됩니다.

 

그럼 저 20이라는 값은 누가 보관하느냐 입니다. 힙이 보관할수는 없겠죠 아까와 같은 이유로...

그럼 어디에 저장이 되는가 하면...

 

어 아까와 똑같은 코드에서 생성자만 추가했는데 갑자기 추가로 8바이트를 더할당해버렸습니다...

그리고 0a, 14의 값이 저장된 a와 b의 위에 14가 또 추가되었네요? 이값이 의미하는 바가 뭘까요..?

이 값이 바로 0x14 == 20이죠 배열의 크기입니다. 생성자, 소멸자의 카운팅 갯수가 되는겁니다.

물론 언어에서 이걸 공식적으로 규격을 정하진 않았습니다.

제가 첫번째 글에서도 말씀드렸듯이 이건 windows, visual studio2019 환경에서만 이런걸수도 있습니다.

사실 컴파일러 구현자에 따라서 저 앞에 바이트를 4로해도 8로해도 상관없을겁니다. 일단 최소한 포인터 크기보다는 커야될겁니다... 왜냐면 할당받을 수 있는 사이즈에 대한 값이니까요...

 

그럼 어셈블리로 넘어가서 한번 확인을 해봐야겠어요... 도대체 왜 0x148만큼을 할당했는지 난 분명 16바이트짜리 20개 320바이트 즉 0x140만 할당을 요청했는데 말이죠...

 

오 이것때문이었습니다... 보세요 ecx에 148h를 담았습니다 컴파일러 스스로 8바이트를 더 달라고 요청한거라구요...

왜냐면 컴파일러가 어딘가에 값을 쓰긴 해야되지만 힙이 관리하는 영역은 이미 os의 영역이기 때문에 상위 레벨 언어인 c++은 접근을 할 수없는 영역이란 말입니다... 그렇다보니 스스로 8바이트를 더 할당해서 앞 8바이트를 몇칸인지에 대한 정보로 쓰고 나머지 140바이트 즉 block + 8을 반환하는겁니다...

 

그럼 하나가 또 궁금한게 있죠..

delete[] 의경우에는 4바이트 혹은 8바이트만큼 가서 하는가?

네 그렇습니다. delete의 경우에도 앞 4바이트나 8바이트를 찾아가서 거기를 실제 free합니다.

네 operator delete를 하기전에 인자로 전달되는 rax - 8 블록으로 들어온 포인터 -8로 가서 10h을 곱한걸 rax에 저장하고 그 rax에 8을 더합니다.

즉 할당받았던 블록에서 8바이트 전으로 가서 소멸자 카운팅을 가져온 다음에 거기에 사이즈를 곱하고 + 8바이트를 해서 그 바이트수 만큼 deacllocate을 해주는 operator delete(void* const block, const size_t size); 함수를 호출하는거죠.

 

그렇다면 생성자와 소멸자가 있는경우 앞에 pointer의 크기만큼(windows vs19기준) 

컴파일러가 추가로 할당을 해준다는걸 알았습니다. 그렇다면 new와 delete, new[]와 delete[]를 혼동해서 쓴다면 어떤일이 일어나게될까요..?

 

뭐 이미 정리는 다 했습니다.

그대로 생각하시면됩니다.

 

new -> delete[]의 경우에는

할당 문제없고 delete[]를 할때 소멸자를 호출하기 위해서 앞 8바이트 혹은 4바이트를 읽을건데 그 앞에 어떤값이 있는지 모릅니다... 어마어마하게 큰값이 들어있을 확률이 높죠...

그래서 소멸자를 어마어마하게 많이 호출할겁니다. 만약에 중간에 멤버변수를 접근하는 코드가 있다면 당연히 터지겠죠 하지만 멤버변수를 접근하지 않는다면 어마어마하게 긴 시간이후에 메모리 반환을 하려고 할겁니다... 어라 근데 제가 new로 할당받을때는 따로 카운팅을 넣지 않으니까 그 포인터가 곧 할당지점이 되는건데요..? delete[]의 경우에는 할당지점 -8로 가서 지우려고 시도를 하기 때문에 힙에서 터져버리게 됩니다.(이런경우에는 에러를 찾으려고 한다면... 앞4,8바이트 뒤 4,8바이트 모두 싹 뒤지셔서 찾으면됩니다.

 

 

new[] -> delete의 경우에는

할당 문제없고 delete를 하려고 할때 소멸자 한번 호출할겁니다. 그리고 삭제하려고 했는데 잘못된 포인터가 들어왔죠?실제 할당된 포인터는 block - 8인데 우리는 block을 지우려고 하고있으니... 여기서 힙 터지는겁니다.

 

단순하죠..? 딱 생각한 고대로 나오지 않습니까 ㅋㅋㅋ...

 

음... 일단은 오늘 내용은 저도 사실 처음 배웠을때는 이해하기 상당히 힘들었던 부분이었습니다.

왜냐면 저는 게임 공학과 학생이라 유니티 언리얼 이런거나 했지.. 컴공, 소공 이런과 학생이 아니라서 어셈블리나 메모리뷰같은걸 볼줄 몰랐었거든요 심지어 os나 컴퓨터 구조 이런것도 전혀 몰랐구요...

 

아마 이글을 처음부터 끝까지 다 읽으셨고 이 내용을 완벽히 숙지 하신다면... 솔직히 c++ 동적할당에 대해서는 걱정안하셔도 될정도라고 생각합니다 저는...

 

그럼 오늘도 긴글 읽어주신 여러분들께 감사드립니다.

다음번에는 더 좋은 정보를 들고 돌아오도록 하겠습니다.

그럼 안녕히계세요~~

320x100

'c,c++ 기본 > next step' 카테고리의 다른 글

class에 대한 얘기(2)  (0) 2021.11.24
template와 class에 대한 얘기(1)  (3) 2021.11.23
메모리와 c++에 관한 얘기  (0) 2021.11.14
DLL,LIB, FILEIO에 대한얘기  (0) 2021.11.13
게임서버 이론1  (0) 2021.11.09