3장. 자원관리 👥

안녕하세요! 두두코딩 입니다 ✋
오늘은 new 와 delete 개념에 대해 알아보겠습니다.

🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!

궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇

new 와 delete의 동작

...

std::string *stringArray = new std::string[100];

...

delete stringArray;

위의 코드는 정상적으로 컴파일이 된다. 혹시 틀린부분이 있는것 같은가? 맞다고 한다면 아주 좋은 관점을 갖고 있는 사람이라고 생각한다. 해당 코드는 컴파일은 잘되지만, 미정의 동작을 수행한다. 위의 코드의 동작이 정의 되어있지 않지만, 정말 잘 동작했다고 가정을 해보자.

잘 동작했다고 가정을 하더라도, 100개의 string 객체 가운데, 하나의 객체만 소멸되고, 99개의 나머지 객체는 소멸되지 않을 가능성이 크다.

해당 문제를 해결하기 앞서, newdelete 동작에 대해 먼저 알아보자.

🌱 new 연산자 호출

new 연산자를 사용해 표현식을 꾸미게 된다면 내부적으로 2가지 동작이 발생한다.

  1. 메모리 할당
  2. 생성자 호출

위의 순서대로 동작한다는 것을 잘알아두자. 나중에 operator new()연산에 대해 다룰일이 올 것이다.

🌱 delete 연산자 호출

delete 연산자를 호출하는 것도 내부적으로는 2가지 연산이 발생한다.

  1. 소멸자 호출
  2. 메모리 해제

new연산과는 반대로 동작하지만, 순서를 잘 기억해두기 바란다.

위의 순서로 보면, delete연산자가 한번 불릴 때, 소멸자가 한번 불리고, 메모리가 해제되는 것을 알 수 있다. 이 말은, delete 연산이 적용되는 객체의 갯수는 소멸자 호출의 횟수와 동일하다고 볼 수 있다.

그렇다면 delete연산자를 호출 했을 때, 삭제되는 포인터가 객체 하나를 가리킬까 아니면 객체 배열 전체를 가리킬까?

가리키는 포인터에 따라, 소멸자가 몇 번 불리는지 알 수 있고, 소멸자의 호출 횟수에 따라 몇 개의 객체가 자원을 해제했는지 알 수 있다. 가리키는 포인터는 어떻게 알 수 있을까? 해당 포인터를 알기 위해서는 객체의 메모리 구조를 알아야한다.

단일 객체와 다중 객체의 메모리 구조

memory structure

위의 그림과 같이, 단일 객체와 다중 객체는 메모리 구조가 다르다. 단일 객체일 경우 메모리 영역에 Object 크기만큼 할당돼 객체가 형성되는 것을 볼 수 있다. 만약 다중 객체일 경우 다수의 Object가 만들어 질 뿐만 아니라 초기에 몇 개의 객체가 존재하는지를 표기하는 n이 존재한다. (여기서 n의 크기는 배열의 크기 객체의 갯수이다.)

모든 컴파일러가 동일한 자료구조로 형성되어져있는건 아니지만, 대부분의 컴파일러는 해당 그림과 같이 구현되어져 있다.

따라서, delete연산자를 수행할 때, 다중 객체일 경우 해당 n값을 읽어 그만큼 소멸자를 호출해주고 자원을 해제해주면 된다. 만약 단일 객체일 경우 해당 object만 제거하면 된다.

그렇다면, 어떻게 단일 또는 다중 객체인지 구별할 것인가?

구별하는 방법은 바로 개발자에게 있다. 개발자가 []라는 키워드를 delete뒤에 붙여 적어주면 “이 친구는 다중 객체니까 n을 참고해서 소멸자 호출하고 자원 해제해” 라고 알려주는 것이다. 만약 []를 붙이지 않으면 단일 객체니까 그냥 소멸자 호출하고 메모리 해제해 라고 하는 것이다.

std::string *stringPtr1 = new std::string;

std::string *stringPtr2 = new std:;string[100];

...

// 객체 한 개를 삭제한다.
delete stringPtr1;

// 객체 배열을 삭제한다.
delete[] stringPtr2;

위의 예시를 보면, 단일 객체일 때 delete를 사용하고, 다중 객체일 때 delete[]를 사용하고 있는 것을 볼 수 있다.

만약 두개를 바꿔서 사용하게 된다면 어떻게 될까?

💣 단일 객체에 delete[] 사용할 경우

만약 stringPtr1delete[]를 사용할 경우 어떻게 될까? 우선 delete[]는 앞쪽의 메모리 몇 바이트를 읽고 해당 데이터가 배열 크기 (객체의 갯수)라고 생각하고 소멸자 호출을 수행할 것이다. 소멸자를 수행하는 과정에서 본인의 객체가 아니라는 것을 알게 되며 에러를 만나게 될 것이다.

💣 다중 객체에 delete 사용할 경우

만약 stringPtr2delete를 사용할 경우는 어떻게 될까? 처음에 언급했지만, 미정의 동작이 수행될 것이다. 해당 원인이 너무 명확(소멸해야되는 객체의 수가 너무 작다)하다는 것을 우리가 알 수 있다. 에러가 날수도 안날수도 있지만 좋지 못한 프로그래밍 방법이라는 것은 확실하다.

우리는 배열을 동적할당으로 받게되면 무조건 delete[]를 사용해야된다는 점만 기억하면 된다. 객체가 아닌 기본 타입 int일 경우에도 동적할당을 받으면 delete[]로 제거해야한다. 이 친구는 소멸자도 없는데 그렇게 해야한다.. ㅠㅠ (이건 규칙이라 생각하고 외우는 것이 좋다.)

typedef의 주의점

우리가 규칙이라 칭한 내용을 typedef를 사용하면 엄청난 주의를 기울여야한다.

예를 들어보자.

typedef std::string AddressLines[4];

// 해당 코드는 아래와 같이 치환된다.
// std::string *pa1 = new std::string[4];
std::string *pa1 = new AddressLine;

// 아래의 delete가 아닌 delete[]를 사용해야함.
// delete pa1;
delete[] pa1;

위와 같이, typedef로 정의할 경우 전처리 과정에서 치환된 후 컴파일을 하게 된다. 따라서, 다중 객체를 할당받게 되며, 우리는 delete[]연산으로 처리해야한다. 하지만, new를 이용해 할당하는 과정에서 typedef로 치환된 인자를 적다보면, 단일 객체로 착각할 수도 있다. 따라서, typedef를 사용해야하는 개발자라면 delete연산에 주의를 요할 필요가 있다!!

동적할당에 대해 챙겨야할게 너무많다. 라고 생각할 수 있다. 하지만, C++에서는 이런 어려움을 처리하게 위해 Container를 만들어 놓았으니 그건 바로 vectorstring이다. vector<string>으로 정의를 하게 되면 delete연산에 대해 주의는 필요 없으니 실수를 줄일 수 있는 컨테이너를 자주 애용하도록 하자.

To Sum Up

👉 new 표현식에 [] 썼다면 delete[]를 써야함.

👉 new 표현식에 [] 안 썼다면 delete를 써야함.