객체 생성 방법
C++에서는 객체를 생성할 때 두가지 방법을 사용할 수 있다.
각 방법에 따라 객체가 할당되는 메모리 영역이 달라진다.
1. Stack에 할당
일반적인 변수와 같이 선언한다.
Foo obj1 = Foo(5);
Foo obj2(10);
2. Heap에 할당
new 키워드를 사용하여 선언한다.
Foo* obj3 = new Foo(20);
운영체제 시간에 배웠듯이,
Stack 메모리 영역은 변수를 선언한 스코프 영역을 벗어나면 자동으로 메모리가 해제되지만,
Heap 메모리 영역은 프로그래머가 관리해야 하는 영역이므로 자동으로 해제가 되지 않는다.
위 코드의 obj3을 보면, new 키워드를 이용해 Heap 메모리에 객체를 할당하고 있다.
포인터는 Stack에 있고, 실제 객체의 데이터는 Heap에 담겨 있어서 Stack에 있는 포인터가 Heap을 가리키는 형태이다.
그런데 만약 메모리 해제(delete)를 하지 않고, 해당 스코프를 벗어나면 어떻게 될까?
포인터는 Stack에 있네? 스코프 벗어났으니까 해제해줘야지.
근데 실제 데이터는 Heap에 있네? 근데 해당 Heap에 접근할 포인터가 사라졌네?
접근이 안되는데 해제를 해줄 수 있을리가 ...
이러면 메모리 누수가 나게 되는 것이다.
따라서, Heap 메모리에 객체를 할당할 경우 메모리 누수(Memory Leak)가 나지 않게 각별한 주의가 필요하다.
1. new로 객체를 생성했으면 delete를 반드시 써주던가.
2. 스마트 포인터를 이용해 자동으로 메모리를 해제할 수 있게 해주던가.
예제
#include <iostream>
using namespace std;
class Foo
{
private:
int num;
public:
Foo(int _num) : num(_num)
{
cout << this << ": 객체 생성" << endl;
}
void ShowNum()
{
cout << "num: " << num << endl;
}
~Foo()
{
cout << this << ": 객체 소멸" << endl;
}
};
int main()
{
cout << "start main()" << endl;
Foo obj1 = Foo(5); // Stack
Foo obj2(10); // Stack
Foo* obj3 = new Foo(20); // Heap
obj1.ShowNum();
obj2.ShowNum();
obj3->ShowNum();
cout << "obj1 address: " << &obj1 << endl;
cout << "obj2 address: " << &obj2 << endl;
cout << "obj3가 가리키는 주소: " << obj3 << endl;
cout << "end main()" << endl;
delete obj3;
}
예제 코드를 만들어보았다.
obj1과 obj2는 Stack에 할당하는 객체이고,
obj3는 Heap에 할당하는 객체이다.
실행시켜보면, obj1과 obj2는 Stack에 할당되었기에, 스코프를 벗어나면 자동으로 소멸자가 호출되는 것을 볼 수 있다.
반면 obj3는 소멸자가 호출되지 않고 있다.
스코프를 벗어나면 해당 메모리에 더이상 접근할 수 있는 방법이 없기에 메모리 누수가 나게 된다.
int main()
{
cout << "start main()" << endl;
Foo obj1 = Foo(5); // Stack
Foo obj2(10); // Stack
Foo* obj3 = new Foo(20); // Heap
obj1.ShowNum();
obj2.ShowNum();
obj3->ShowNum();
cout << "obj1 address: " << &obj1 << endl;
cout << "obj2 address: " << &obj2 << endl;
cout << "obj3가 가리키는 주소: " << obj3 << endl;
cout << "end main()" << endl;
delete obj3;
}
메모리 누수를 막기 위해 맨 마지막 줄에 delete 연산자를 통해 메모리 해제를 시켜주었다.
소멸자가 호출되는 것으로 보아 메모리가 잘 해제되고 있다.
그러면 각각 언제 사용해야할까?
Heap
- 크기가 큰 객체를 할당할 때
- Stack 메모리는 거의 디폴트로 1MB 정도의 크기를 갖는데, 이 크기를 벗어나는 객체를 할당할 때 Heap을 사용해야 한다.
- 동적 할당할 때
- vector와 같이 동적으로 변하는 배열의 경우 Heap에 할당해야 한다.
- 리스코프 치환을 만족하는 객체를 할당할 때
두번째와 세번째 경우는 런타임과 관련된 이야기인데,
Stack은 컴파일 시에 모든 것을 결정해야하기 때문에 사이즈가 계속 변하는 배열을 사용할 때나, 런타임에 타입을 결정하는 경우에 Stack을 사용할 수 없다.
(이 부분은 더 조사하여 추가할 것이다.)
Stack
Heap에 할당해야 하는 상황을 제외하고 모두 Stack에 할당한다.
Heap 메모리 할당은 Stack 할당보다 느리고, 메모리 누수 문제도 있어 위와 같은 상황이 아니면 굳이 사용할 필요가 없다.