[C++] C++의 Stack Instance 와 Heap Instance 비교
카테고리: C++
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 Address 와 main() Heap Instance Address 두개의 주소만 동일하다는것을 알 수 있습니다.
그리고, Stack / Heap 에 상관없이 int 에 할당한 값은 같다는것을 볼 수 있습니다.
따라서 다음 결론을 내릴 수 있습니다.
Stack
- 함수 안에서 생성된 지역 변수는 해당 함수의 실행 동안만 유효하다.
- 지역 변수는 스택(Stack) 메모리에 할당되며, 고유한 주소를 가진다.
- 함수가 종료되면 지역 변수는 스택에서 제거되어 메모리에서 사라진다.
- 따라서 지역 변수를 반환하면, 반환 시점에는 그 값이 복사되어 다른 함수의 지역 변수에 저장되고, 그 주소는 당연히 달라진다.
Heap
new를 사용해 동적으로 생성된 객체는 힙(Heap) 메모리에 할당된다.- 이 객체를 가리키는 포인터 변수는 지역 변수이므로, 스택에 저장된다.
- 함수가 종료되면 포인터 지역 변수는 스택에서 제거되지만, 힙에 있는 객체 자체는 그대로 남아있다.
- 다른 함수(예:
main)에서 반환받은 포인터를 저장하면, 그 포인터는 힙에 있는 같은 객체를 계속 가리키게 된다. - 즉, 포인터 지역 변수의 주소는 달라질 수 있지만, 힙에 생성된 객체의 주소는 변하지 않는다.
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 형의 예제와 같은 방식으로 작동되고 있는것을 알 수 있습니다.
결론
위 과정을 요약하면 다음과 같습니다.
- 스택 변수는 함수가 끝나면 사라지고, 다른 함수에 반환할 때는 값 복사가 일어납니다.
- 값 복사가 일어나면 메모리에서는 불필요한 삭제와 생성을 반복하게 됩니다.
- 힙 객체는 함수가 끝나도 메모리에 남아 있으며, “포인터”를 통해 그 객체에 접근할 수 있습니다.
따라서, 한번 쓰고 버릴 값이 아닌, 여러군데에서 한 객체에 접근해야 한다고 하면, Stack에 생성되는 Instance가 아니라 Heap에 생성되는 Instance를 사용하는것이 더 좋습니다. 반대로, 금방 쓰고 버릴 객체는 Stack에 생성해 주는게 좋습니다.
하지만 Heap 객체는 개발자가 직접 해제하기 전까지는 계속 Heap에 상주하고 있기 때문에 해제해주는 과정을 까먹지 말아야 합니다.
댓글 남기기