9.2주차
destructor
constructor와 반대로 메모리에서 사라질 때 부르는 것을 소멸자라고 한다.
메모리에서 사라지는 경우는 가까운 중괄호가 닫힐 때 사라진다. 이때는 소멸자는 내가 명시적으로 정할 수 없다.
#include <iostream>
using namespace std;
class Sample
{
int n;
public:
Sample();
~Sample();
Sample(int x);
void setN(int x);
int getN();
};
Sample::Sample()
{
n = 0;
cout << "Sample() is Called" << endl;
}
Sample::Sample(int x)
{
n = x;
cout << "Sample() is Called" << endl;
}
void Sample::setN(int x)
{
n = x;
}
int Sample::getN()
{
return n;
}
Sample::~Sample()
{
cout << n << " ~Sample() is Called" << endl;
}
int main()
{
Sample x, y;
Sample z(10);
Sample *w;
w = new Sample(1000);
x.setN(1);
y.setN(2);
cout << "x.n: " << x.getN() << endl;
cout << "y.n: " << y.getN() << endl;
cout << "z.n: " << z.getN() << endl;
delete w;
}
>>>
Sample() is Called
Sample() is Called
Sample() is Called
Sample() is Called
x.n: 1
y.n: 2
z.n: 10
1000 ~Sample() is Called
10 ~Sample() is Called
2 ~Sample() is Called
1 ~Sample() is Called
이렇게 된다면 명시적으로 소멸자를 부른 w부터 소멸되고, 그 다음 스택에 넣어져 있는 z, y, x 순으로 자동으로 메모리에서 소멸된다.
int main()
{
Sample x;
Sample *w;
w = new Sample();
{
delete w;
}
}
여기서 new를 통해서 동적 할당을 받을 수가 있다. 동적 할당을 받게 되면 Local 변수로 힙에 들어가게 된다. 로컬 변수의 경우는 스택에 쌓이게 되고, 동적 할당된 경우는 힙에 할당이 된다. 힙에서 사라지도록 만들기 위해서는 명시적으로 delete를 해야 된다. 만약에 이렇게 w를 delete해버리면 w가 가리키고 있던 Sample의 멤버 변수들이 힙 영역에서 사라지고 stack 영역에 있던 w는 자동으로 사라진 것이다.
자바의 경우는 생성자가 없다. 왜? 소멸자도 없으니까. 자바는 delete를 프로그래머가 못하고 가비지 콜랙터가 알아서 정하기 때문에 따로 명령어가 없다. 자바는 프로그래머가 할당 받은 메모리를 언제 반납할지 정하지 않고 프로그램 상에서 가비지 콜랙터가 그 역할을 하게 되어 자동으로 힙의 영역의 메모리를 삭제해준다.
C++은 힙에 있는 메모리는 언제 반납할지를 delete라는 명령어를 통해서 프로그래머에게도 권한을 준다. 메모리는 잡혀있는데 어디에 있는지 모르는게 memory leak 이다. Memory Leak란 프로그래머가 delete를 까먹에 되면 그 메모리는 잡혀있는데 사용을 못하는 메모리를 뜻한다. 비유 천재님께서 비유를 들자면 코코랑 비슷하다고 했다. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 코코에서 영혼은 자신을 기억하는 사람이 아무도 없을 때 영혼도 사라지게 된다고 했다. 즉, 스택 영역에서 메모리를 기억하는 변수가 없다면 자바의 경우는 이때 가비지 콜랙터가 알아서 지워 주게 된다. 여기서 가비지 콜랙터도 너무 바쁘기 때문에 그때그때 지우는 것이 아니라 스케쥴을 잡아서 지우는데 변수는 지워지고, 메모리에 남아있을 그 잠시동안 메모리 리크가 발생할 수 있다. 그렇긴 하지만 여튼 가비지 콜랙터가 자동으로 지워준다는 점! 하지만 C++의 경우는 이승 세계에서 기억해주지 않는 것은 그냥 너의 운명이다. 라는 마인드로 기냥 메모리만 차지한 상태로 냅두게 된다.
int main()
{
Sample x, y;
Sample z(10);
Sample *w;
w = new Sample(1000);
{
delete w;
}
x.setN(1);
y.setN(2);
cout << "x.n: " << x.getN() << endl;
cout << "y.n: " << y.getN() << endl;
cout << "z.n: " << z.getN() << endl;
}
>>>
Sample() is Called
Sample() is Called
Sample() is Called
Sample() is Called
1000 ~Sample() is Called
x.n: 1
y.n: 2
z.n: 10
10 ~Sample() is Called
2 ~Sample() is Called
1 ~Sample() is Called
힙에 들어간 메모리는 delete되는 순간에 사라진다. 따라서 위 코드를 돌려보게 된다면 중간에 destructor에 의해서 지워지는 것을 볼 수 있다. 얘는 생성자를 생성하자 마자 소멸자를 불렀기 때문이다.
근데 따지고 보면 Sample *w는 그냥 포인터 변수이기 때문에 Sample 생성자와 상관이 없다. 실제로 Sample이라는 인스턴스가 만들어지는 코드는 new Sample();
이 된다. 그렇다면 w는 단순히 포인터를 저장하기 위한 변수로서 4바이트 만을 차지한다. 따라서 포인터 변수 w는 생성자를 호출하지 않게 된다. 스택의 경우는 중괄호가 끝날 때, 힙의 경우는 delete가 명시적으로 호출될 때 사라지게 된다. 그렇기 때문에 스택은 기계적으로 생겼다 지워졌다가 가능하다. 하지만 힙의 경우는 프로그래머가 직접 지워주어야 된다.
프로그래머 맘대로 생성, 소멸을 결정할 수 있기 때문에 동적 할당을 위한 저장 공간을 힙이라고 하는 것이다. 힙은 말 그대로 더미라는 뜻으로 그냥 막 쌓였다가 중간에 하나 없어지고 그런 것을 의미한다. 따라서 C++의 경우는 class 객체를 스택에 둘지 힙에 둘지 선택할 수 있다.
❓만약에
int main()
{
Sample *w;
Sample x, y;
Sample z(10);
w = new Sample(1000);
{
delete w;
}
x.setN(1);
y.setN(2);
cout << "x.n: " << x.getN() << endl;
cout << "y.n: " << y.getN() << endl;
cout << "z.n: " << z.getN() << endl;
}
이 순서대로 선언을 변수를 만든다면 스택에는 *w -> x -> y -> z 순으로 들어가 있는데, 힙 영역에 있는 멤버 변수가 delete로 지워지게 되면 w는 어떻게 되는 건지? LIFO에 따라 z, y, x가 지워지고 나서야 w가 지워지는 건가?
❓x, y, z의 멤버 변수는 어느 영역에 어떻게 할당이 되는가?
Sample x; // -> 스택에 저장
Sample *w; // -> 스택에 저장
w = new Sample(); // -> 힙에 저장
w는 원래 포인터 변수이므로 결국 로컬한 포인터 변수가 되고, 즉, w는 중괄호가 닫히면 그 때 값이 사라진다. 근데 w가 가리키고 있었던 주소는 아직도 메모리를 차지하고 있다. 포인터 변수 자체는 스택에 있는데, 걔가 가리키고 있는 메모리는 힙에 있다. 이런 상황에서 w가 지워진다면 메모리 릭이 발생한 것이다. 힙에는 메모리가 잡혀있는데 이를 기억하고 있던 w가 지워졌기 때문에! 결국 메모리 리크가 발생하지 않게 하기 위해서는 중괄호가 끝나기 전에 다른 변수에 전달 전달 하는 식으로 해야 된다.
그렇다면 왜 반드시 소멸자를 사용해서 지워야되나. w는 어차피 사라질 친군데 (? 이거 맞아?) delete w를 통해서 w가 가리키는 메모리 영역을 해제하게 된다. 그 다음 자동적으로 w 변수가 팝이 된다. 즉, 메모리를 해제하고 open된 파일을 close 해주는 등의 뒤처리를 delete를 통해서 명시적으로 진행이 가능하다. 그 다음 스택에 있는 w 변수가 팝되어 사라지는 과정을 거치면서 메모리 릭을 없앤다.
copy constructor
#include <iostream>
using namespace std;
class Sample
{
int n;
public:
Sample();
~Sample();
Sample(int x);
void setN(int x);
int getN();
};
Sample::Sample()
{
n = 0;
cout << "Sample() is Called" << endl;
}
Sample::Sample(int x)
{
n = x;
cout << "Sample() is Called" << endl;
}
void Sample::setN(int x)
{
n = x;
}
int Sample::getN()
{
return n;
}
Sample::~Sample()
{
cout << n << " ~Sample() is Called" << endl;
}
int main()
{
Sample z(10);
Sample copy(z);
cout << "z.n: " << z.getN() << endl;
cout << "copy.n: " << copy.getN() << endl;
}
>>>
Sample() is Called
z.n: 10
copy.n: 10
10 ~Sample() is Called
10 ~Sample() is Called
인스턴스의 매개변수가 해당 클래스인 경우가 잇다. 이게 뭐냐면 생성자 중에서 copy 생성자라고 한다. 나 자신을 매개 변수로 받게 되면 copy constructor는 모든 멤버 변수를 자동적으로 복사해준다. 따라서 z에 넣었던 n값이 copy 의 n에 copy된 것을 확인할 수 있다.
copy constructor는 어떻게 생겼는지 확인하기 위해서 작성한 코드이다.
#include <iostream>
using namespace std;
class Sample
{
int n;
public:
Sample();
Sample(int x);
Sample(Sample &a);
~Sample();
void setN(int x);
int getN();
};
Sample::Sample()
{
n = 0;
cout << "Sample() is Called" << endl;
}
Sample::Sample(int x)
{
n = x;
cout << "Sample() is Called" << endl;
}
// copy constructor
Sample::Sample(Sample &a)
{
n = a.n + 1000;
}
void Sample::setN(int x)
{
n = x;
}
int Sample::getN()
{
return n;
}
Sample::~Sample()
{
cout << n << " ~Sample() is Called" << endl;
}
int main()
{
Sample z(10);
Sample copy(z);
cout << "z.n: " << z.getN() << endl;
cout << "copy.n: " << copy.getN() << endl;
}
>>>
Sample() is Called
z.n: 10
copy.n: 1010
1010 ~Sample() is Called
10 ~Sample() is Called
copy constructor는 결국 생성자는 생성자 형태이긴 한데, 그 안에 매개변수로 자기 자신을 받는다. copy가 되었음을 확인하기 위해서 기존 값보다 1000을 더하여 출력하는 식으로 확인할 수 있다.
근데 이 경우는 결국 생성자를 이용한 초기화이므로 인스턴스가 생셩된 순간에만 사용할 수 있다. 하지만 그 이외에도 생성 이후에 값을 복사할 일이 있을 수 있다. 그럴 경우는 = 연산자를 사용해서 멤버 변수들의 값을 복사할 수 있다. 이는 c++의 operation OverLoading으로 인해서 가능한 연산이다. 오퍼레이더 오버로딩은 나중에 설명 예정
또 다른 초기화 방법으로는
Sample::Sample(int x) : n{x} {
cout << "init" << endl;
}
과 같은 방법도 있는데, 교수님 왈 굳이? 뭐하러 복잡한 문법을 쓰냐,,, 교수님은 이거를 권장하지는 않음.
this
Sample::Sample(int n) {
n = n;
cout << endl;
}
만약에 파라미터가 n이 되버리면 n = n이 되버린다. 스코프가 다른 경우 같은 변수의 이름을 선언 및 정의를 할 수 있었다. 하지만 해당 스코프에서 사용되는 n은 가까이 있는 n을 기준으로 사용이 되기 때문에 결국 파라미터 n = 파라미터 n으로 값을 초기화 하는 의도한 결과가 나오지 않게 된다. 따라서 이럴 경우 this를 사용하게 된다. 따라서 this->m
을 통해서 왼쪽에 있는 n은 현재 클래스의 멤버 변수를 뜻하고 있음을 나타낸다.
여기서 든 의문점은 this 다음 화살표이다. 자바의 경우는 클래스 객체는 무조건 힙으로 사용되기 때문에(인스턴스는 반드시 new를 사용하므로 동적 할당에 해당) 자바에서는 바로 .을 사용해서 상관이 없었다.
하지만 클래스도 스택, 힙 모두 메모리를 차지할 수 있는 경우 스택에 정의된 변수는 .을 사용하고 힙에 정의된 메모리는 -> 를 사용해서 변수에 접근할 수 있다. this는 자동으로 현재 자신의 객체를 가리키기 때문에 ->를 사용해서 힙 영역에 있는 메모리 변수에 접근을 한다.
❓
근데 스택 영역에 정의된 인스턴스를 만들 수 있는데, 얘는 힙에 있는 것이 아님에도 불구하고 this는 화살표를 통해서 접근? -> 코드상 되긴 한다. 근데 왜? 왜 되는 거지?
댓글남기기