[C++] C++의 Stack Instance 와 Heap Instance 비교

Date:     Updated:

카테고리:

태그:

C++는 대표적인 객체지향 언매니지드(Unmanaged) 언어입니다.
언매니지드 언어란 가비지 컬렉터(GC)가 메모리를 자동으로 관리해 주지 않고, 개발자가 직접 메모리를 할당하고 해제해야 하는 언어를 의미합니다.

반면, 매니지드(Managed) 언어에서는 객체를 생성할 때 모든 객체가 힙(Heap)에 할당되며, 변수에는 해당 힙 객체의 주소(참조값)가 저장됩니다. 즉, 매니지드 언어의 객체는 모두 참조 타입(Reference Type) 으로 동작하게 됩니다.

하지만 C++는 이와 달리 객체를 스택(Stack) 에 직접 생성할 수도 있습니다. 따라서 스택에 생성된 객체와 힙에 생성된 객체는 동작 방식과 수명 관리에서 차이가 발생합니다.

이 차이를 좀 더 명확히 이해하고자, 본 예제를 작성해 확인해 보았습니다.

int 자료형의 Stack / Heap 생성 차이

우선, int는 C++의 대표적인 기본 자료형 입니다. 그리고, 기본적으로 int형의 변수를 만들고 값을 넣으면, Stack 메모리에 공간을 할당하여 값을 집어넣는다 라고 알고 있습니다.

그리고 동적할당을 배우고 난 뒤에는 int* 형으로 변수를 생성하고, 거기에 new int()를 통해서 Heap에 객체를 만들고, Stack 메모리에 int* 변수를 만들어 Heap에 만들어진 객체의 주소를 담도록 되어 있습니다.

아래는 함수를 통해서, int 형 변수를 return 하는것과 int* 형 변수를 return 할 때, 메모리에서 어떤 동작이 일어나는지 확인해보는 예제 입니다.

#include <iostream>

class SomeClass
{
public:
	int Number = 0;
};

int GetInt()
{
	int someInt = 20;
	std::cout << "GetInt() Local Variable Address : " << &someInt << std::endl;
	std::cout << "GetInt() Local Variable Value : " << someInt << std::endl;
	return someInt;
}

int* GetIntPtr()
{
	int* someIntPtr = new int();
	*someIntPtr = 200;
	std::cout << "GetIntPtr() Local Variable Address : " << &someIntPtr << std::endl;
	std::cout << "GetIntPtr() Instance Address : " << someIntPtr << std::endl;
	std::cout << "GetIntPtr() Instance Variable Value : " << *someIntPtr << std::endl;
	return someIntPtr;
}

SomeClass GetSomeClass()
{
	SomeClass someClass;
	someClass.Number = 10;
	std::cout << "GetSomeClass() Instance Address : " << &someClass << std::endl;
	std::cout << "GetSomeClass() Instance Number Value : " << someClass.Number << std::endl;
	return someClass;
}

SomeClass* GetSomeClassPtr()
{
	SomeClass* someClass = new SomeClass();
	someClass->Number = 100;
	std::cout << "GetSomeClassPtr() Local Variable Address : " << &someClass << std::endl;
	std::cout << "GetSomeClassPtr() Instance Address : " << someClass << std::endl;
	std::cout << "GetSomeClassPtr() Instance Number Value : " << someClass->Number << std::endl;
	return someClass;
}

int main()
{
	int someInt = GetInt();
	int* someIntPtr = GetIntPtr();

	std::cout << "main() Stack Instance Address : " << &someInt << std::endl;
	std::cout << "main() Stack Instance Number Value : " << someInt << std::endl;
	std::cout << "main() Heap Instance Local Variable Address : " << &someIntPtr << std::endl;
	std::cout << "main() Heap Instance Address : " << someIntPtr << std::endl;
	std::cout << "main() Heap Instance Address : " << *someIntPtr << std::endl;

	delete someIntPtr;

우선 결과값을 보면 다음과 같습니다.

결과를 보면 GetIntPtr() Instance Addressmain() Heap Instance Address 두개의 주소만 동일하다는것을 알 수 있습니다.

그리고, Stack / Heap 에 상관없이 int 에 할당한 값은 같다는것을 볼 수 있습니다.

따라서 다음 결론을 내릴 수 있습니다.

Stack

  1. 함수 안에서 생성된 지역 변수는 해당 함수의 실행 동안만 유효하다.
  2. 지역 변수는 스택(Stack) 메모리에 할당되며, 고유한 주소를 가진다.
  3. 함수가 종료되면 지역 변수는 스택에서 제거되어 메모리에서 사라진다.
  4. 따라서 지역 변수를 반환하면, 반환 시점에는 그 값이 복사되어 다른 함수의 지역 변수에 저장되고, 그 주소는 당연히 달라진다.

Heap

  1. new를 사용해 동적으로 생성된 객체는 힙(Heap) 메모리에 할당된다.
  2. 이 객체를 가리키는 포인터 변수는 지역 변수이므로, 스택에 저장된다.
  3. 함수가 종료되면 포인터 지역 변수는 스택에서 제거되지만, 힙에 있는 객체 자체는 그대로 남아있다.
  4. 다른 함수(예: main)에서 반환받은 포인터를 저장하면, 그 포인터는 힙에 있는 같은 객체를 계속 가리키게 된다.
  5. 즉, 포인터 지역 변수의 주소는 달라질 수 있지만, 힙에 생성된 객체의 주소는 변하지 않는다.

class 자료형의 Stack / Heap 생성 차이

그렇다면, 해당 로직이 사용자 정의 자료형 class를 생성할때도 같은지 확인해보겠습니다.

#include <iostream>
class SomeClass
{
public:
	int Number = 0;
};

SomeClass GetSomeClass()
{
	SomeClass someClass;
	someClass.Number = 10;
	std::cout << "GetSomeClass() Instance Address : " << &someClass << std::endl;
	std::cout << "GetSomeClass() Instance Number Value : " << someClass.Number << std::endl;
	return someClass;
}

SomeClass* GetSomeClassPtr()
{
	SomeClass* someClass = new SomeClass();
	someClass->Number = 100;
	std::cout << "GetSomeClassPtr() Local Variable Address : " << &someClass << std::endl;
	std::cout << "GetSomeClassPtr() Instance Address : " << someClass << std::endl;
	std::cout << "GetSomeClassPtr() Instance Number Value : " << someClass->Number << std::endl;
	return someClass;
}

int main()
{
	SomeClass someClass = GetSomeClass();
	SomeClass* someClassPtr = GetSomeClassPtr();

	std::cout << "main() Stack Instance Address : " << &someClass << std::endl;
	std::cout << "main() Stack Instance Number Value : " << someClass.Number << std::endl;
	std::cout << "main() Heap Instance Local Variable Address : " << &someClassPtr << std::endl;
	std::cout << "main() Heap Instance Address : " << someClassPtr << std::endl;
	std::cout << "main() Heap Instance Number Value : " << someClassPtr->Number << std::endl;
	
	delete someClassPtr;

	return 0;
}

아래는 출력 결과 입니다.

이 출력결과도 마찬가지로, Heap 메모리에 생성된 객체를 가르키는 포인터의 주소만 같고, 그 외에 지역변수들은 모드 주소가 다르게 할당된것을 볼 수 있습니다.

따라서, int 형의 예제와 같은 방식으로 작동되고 있는것을 알 수 있습니다.

결론

위 과정을 요약하면 다음과 같습니다.

  1. 스택 변수는 함수가 끝나면 사라지고, 다른 함수에 반환할 때는 값 복사가 일어납니다.
  2. 값 복사가 일어나면 메모리에서는 불필요한 삭제와 생성을 반복하게 됩니다.
  3. 힙 객체는 함수가 끝나도 메모리에 남아 있으며, “포인터”를 통해 그 객체에 접근할 수 있습니다.

따라서, 한번 쓰고 버릴 값이 아닌, 여러군데에서 한 객체에 접근해야 한다고 하면, Stack에 생성되는 Instance가 아니라 Heap에 생성되는 Instance를 사용하는것이 더 좋습니다. 반대로, 금방 쓰고 버릴 객체는 Stack에 생성해 주는게 좋습니다.

하지만 Heap 객체는 개발자가 직접 해제하기 전까지는 계속 Heap에 상주하고 있기 때문에 해제해주는 과정을 까먹지 말아야 합니다.

C++ 카테고리 내 다른 글 보러가기

댓글 남기기