글 작성자: Sowhat_93

1. 연산자 vs CRT 함수

 

동적할당이라는 단어에 대해서 잘 생각해보아야 한다.우리가 힙이라고 부르는 것은 풀어쓴다면 다음과 같다.

"스택 프레임과 별개로 프로그래머가 내킬때 해제할 물리메모리"

 

동적 할당은 OS자원에 대한 요청이다.

OS에게 "스택 프레임과 별개로 프로그래머가 내킬때 해제할 물리메모리" 에 대한 요청을 한다.

OS에게 물리메모리에 사용에 대한 허락을 받으면,우리는 힙에 할당되었다고 이야기 하는 것이다.

 

핵심은 new와 delete는 연산자이고 malloc 과 free는 CRT 함수이다.

 

new와 delete가 연산자라는 말은, 어디까지나 C++ 컴파일러가 인식하는 키워드 라는 것이다.

컴파일러는 new와 delete를 발견하면 일련의 기계어 동작으로 해석해낸다.

그리고 그 일련의 기계어 동작안에 반드시 들어가는 것이 바로 생성자와 소멸자의 호출이다.

 

생성자와 소멸자의 호출은 컴파일러가 기계어에 넣어둔다.그럼 메모리영역은?잘 생각해보자.

동적으로 메모리 할당에 대한 요청을 하는 기능을 하는 기계어 명령어가 있을까?

그런 명령어는 없다.

new는 연산자라고 했다. 

C++ 코드로 구현되어있다는 이야기이다.

 

컴파일러는 new가 오버로딩 되어있지 않다면 default new를 호출하도록 한다.

컴파일러는 delete가 오버로딩 되어있지 않다면 default delete를 호출하도록 한다. 

default new의 구현내부에서는 malloc을 호출하도록 되어있다.

default delete의 구현내부에서는 free를 호출하도록 되어있다.

 

정리하면, 연산자 자체가 할당 및 해제의 기능을 가진다고 이해하면 곤란하다는 것이다.

new와 delete를 오버로딩하면, 생성자와 소멸자 호출을 제외한 나머지 부분을 구현해야 한다.

여기서 malloc과 free같은 CRT함수를 사용하기도 하고, OS 전용 함수를 사용하기도 한다.

 

2. new[] 와 delete[]

new[] 와 delete[] 사용시

생성자와 소멸자가 알아서 갯수에 맞게 알아서 잘 호출된다.

이건 왜 이럴까?

아래의 코드를 보도록 하자. 

 

#include <iostream>

class Entity
{
private:
	int Num_A = 1;
	int Num_B = 2;
	int Num_C = 3;

public:
	Entity				(){ std::cout << "Consturct" << std::endl; }
	virtual ~Entity		(){ std::cout << "Destroy" << std::endl; }
};

int main()
{
	Entity* pBlock = new Entity[10];
	delete[] pBlock;

}

객체가 잘 만들어 졌다.

이따가, 이 블럭을 해제할때는 소멸자가 10번 호출되어야 한다.

이걸 어디다 저장해두냐?

그럼 저 0A(10)을 마음대로 바꿔버리면 어떻게 될까?

10개인데 9개를 받았다고 바꾸고, 

10개인데 11개를 받았다고 바꿔보자.

	Entity* pBlock = new Entity[10];
	*(reinterpret_cast<char*>(pBlock) - 8) = 9;
	delete[] pBlock;
	Entity* pBlock = new Entity[10];
	*(reinterpret_cast<char*>(pBlock) - 8) = 11;
	delete[] pBlock;

11개로 바꾸면 HEAP CORRUPTION이다.

당연한 결과이다.

9로 바꾼 경우, 소멸자는 9개만 호출된다.

그런데 값은 10개 모두 쓰레기 값으로 바뀐다.

소멸자는 9개가 호출되지만, OS는 그런거 알바없고 pBlock을 회수해 가기 때문이다.

아니 정확히는, 우리가 아는 pBlock 만큼이 아니라,

거기다가 객체카운트로 사용하던 그 공간까지 가져간다. 

 

우리는 지금 delete[] 를 호출할때 pBlock을 그대로 넘겼다.

delete[] 는 구현에 free를 호출하도록 되어있다.

인자로 뭘 넘길까 그럼??

free의 인자로 pBlock의 주소 - 4번지(x64에서는 8번지) 의 값을 넘기는 것이다.

왜? 저것도 결국엔 동적할당 한 메모리 영역인거다.

다 썼다면 저 영역 또한 해제시 OS에게 보고해야하는 영역이다.

이걸 확인해보자.

	Entity* pBlock = new Entity;
	std::cout << _msize(pBlock) << std::endl;
	//24.

	Entity* pBlockArr = new Entity[2];
	std::cout << _msize(pBlockArr) << std::endl;
	//Crash!
	//할당 받은 주소의 시작점은 8byte 이전이다.(x64)

	std::cout << _msize(reinterpret_cast<char*>(pBlockArr) - 8);
	//24 byte * 2 + 8 byte = 56

두번째 _msize에서는 크래시가 난다.

OS입장에서는 준 영역의 시작점이 8byte이전이기 때문이다.

free 또한 호출해보자.

	Entity* pBlock = new Entity;
	free(pBlock);
	//Okay 문제없음.

	Entity* pBlockArr = new Entity[2];
	free(pBlockArr);
	//Crash !

	free(reinterpret_cast<char*>(pBlockArr) - 8);
	//Okay.

new[] 로 할당받은 경우 delete[] 를 이용해 해제하라고

많은 책이 소개하고 있다.

허나 그 이유에 대해서 궁금해하는 이는 많지 않다.

결론만 말하자면 당연히 free 로도 해제가 가능하다.

할당된 동적메모리는 C++ 언어와는 상관없는 이야기임을 기억하자.

OS에게 반환하는거다.

주소만 잘 맞춰서 주면 관계가 없다.

소멸자가 호출되기를 원한다면 C++ 코드로 직접 호출하도록 한다.

	Entity* pBlockArr = new Entity[2];
	for (int i = 0; i < 2; ++i)
		pBlockArr[i].~Entity();
	free(reinterpret_cast<char*>(pBlockArr) - 8);
	//Okay.

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

[C++] C++ 11 mutable  (0) 2022.03.28
[C++] C++11 override, final  (0) 2022.03.28
[C++] const lvalue reference, rvalue reference  (2) 2022.03.23
[C++] C++11 constexpr  (0) 2022.03.20
[C++] 스마트 포인터 unique_ptr, shared_ptr, weak_ptr  (0) 2022.03.19