0. 개요
🤔 들어가기 전
🤔 객체의 생명 주기와 저장 관리는 프로그래밍 언어에서 객체, 변수, 그리고 바인딩의 작동 방식을 이해하는 중요한 측면이다.
🤔 그렇다면 Binding 시의 고려 사항이 있을까?
🤔 Binding 한 Object의 생명 주기는 어떻게 될까?
1. Binding 시의 고려 사항
Names와 the objects를 엮는 것을 binding이라고 한다.
(여기서 object는 우리가 객체지향 프로그래밍의 객체가 아닌 어떠한 things라고 이해하는 것이 좋다.)
- Objects의 생성과 소멸 => 객체의 life time
- Bindings의 생성과 제거 => 바인딩의 life time
- 바인딩 비활성화와 재활성화 (일시적인 unsusable의 개념)
- 변수, 서브루틴, 타입 등의 바인딩에 사용되는 것들에 대한 References
2. 객체 수명 vs. 바인딩 수명
객체의 수명과 해당 바인딩의 수명이 일치하지 않을 수 있다.
즉, 객체는 바인딩보다 오래 지속될 수도 있고, 바인딩은 객체보다 오래 지속될 수도 있다.
ex. 변수를 참조 인수로 함수에 전달할 때 (주로 Fortran이나 C++의 "&" 매개변수를 사용할 때)
함수 내의 매개변수 이름과 변수 간의 바인딩은 변수 자체의 수명보다 짧을 수 있다.
예시를 들기 전 <참조 호출에 대한 오해>
// C언어
swap(&X, &Y)
*값 호출? 참조 호출?
위의 코드는 사실 참조 호출이 아닌 값 호출이다.
C언어는 참조호출 개념이 없다. / C++은 있음.
- 값 호출 Call-by-value (Pass-by-value)이지,
- 참조 호출 Call-by-reference (Pass-by-reference)가 아니다.
* 아래와 같은 C++ 언어의 코드가 있다고 하자.
int f(int &a) {
a
}
int main() {
int b=3;
f(b);
}
이는 참조 호출 (Call-by-reference)를 사용한 코드이다.
이렇게 되면 a와 b는 완전히 같은 메모리 공간을 나타내는 "같은 변수"가 된다.
main 함수의 코드 진행을 보면서 바인딩을 생각해 보자.
b (변수)와 3 (object)의 바인딩은 main 함수 run time 내내 life time이 된다.
하지만, a (변수) 와 3 (object) 의 바인딩은 f(b)라는 함수의 호출 실행 중만이 life time이다.
하지만 f(b)라는 함수가 끝나도, 3이라는 object의 life time은 b와의 바인딩을 통해 계속 유지되고 있다.
이렇듯 참조 호출을 사용하는 경우 등의 상황에서는
바인딩 생명주기 ⊂ 객체 생명주기의 상황이 나타날 수 있다.
* 다음 예시도 보자.
// 일단 이 코드는 실행 가능하기는 하지만, bug의 일부이다.
typedef Test* PTest;
void func(PTest& v) {
int c = v->getValue();
printf("value = %d\n", c);
delete v;
}
int main() {
PTest t = new Test(5);
func(t);
printf("value = %d\n", t->getValue());
}
먼저, 코드를 이해해 보자.
Test 포인터에 대한 새로운 타입인 PTest를 정의하고 있다.
func라는 함수는 PTest 타입의 참조(&)를 인자로 받는다.
함수 내에서는 해당 객체의 getValue 메서드를 호출하고, 객체를 삭제(delete)한다.
'v'라는 이름이 붙어있는 주소로 가서 그 주소가 가리키는 메로기 공간에 있는 객체를 delete 하는 것이다.
그러나 주요 관찰점은 main 함수에서 func(t)를 호출한 후에도 t 변수에 접근하려고 시도하고 있다는 것.
func 함수 내에서 객체를 삭제하면 해당 객체에 대한 메모리가 해제되며, 객체를 가리키는 포인터 (t)가 무효화된다.
객체는 주소가 할당 해제되었지만 (lifetime 끝) , 바인딩은 살아있는 경우이다.
객체 생명주기 ⊂ 바인딩 생명주기
코드가 동작하기는 하지만, 마지막줄에서 무효화된 포인터에 접근하는 것이 되어, 오류를 발생시킬 수 있다.
cf. 쉽게 현실의 예시를 들어보자.
[211호 : 백교수님 연구실] 이라는 정보가 적혀있는 명함이 있다고 하자.
이후 백교수는 방을 뺐다고 생각하자.
Object는 delete 되었지만, 주소에 대한 binding은 유효하지 않을 뿐 (접근 시 문제 발생) binding은 남아있다.
<⚠️ 물론 방 자체(211호 라는 주소)가 사라진 것은 아니다. "백 교수님의 연구실"이라는 Object가 사라진 것이다>
<이후 다른 교수님이 또 그 주소에 들어올 수도 있다.>
+ 또 아주 흔한 현실 세상의 issue를 예시로 들어보자.
네이버 지도에서 맛집을 "My place"에 등록해 놓았다고 가정하자.
서울특별시 송파구 올림픽로32길 38 = 송*옥 (binding)
예를 들어, 해당 매장이 이후에 폐업했다고 가정해 보자. (함수가 끝나고 할당 해제가 된 상황이다)
그러면, 분명 내 플레이스에는 해당 바인딩 (주소와 상호명)이 있지만,
클릭하면 없는 '폐업한 매장'이라고 뜨고, 해당 매장에 직접 찾아가도 해당 매장을 찾을 수 없을 것이다. (접근 시 유효 x)
object는 delete 되었지만, 주소에 대한 바인딩이 남아있는 것이다.
(이 또한, 그 주소에 다른 매장이 들어설 수도 있는 것이다.)
3. Garbage와 Dangling Reference
- Garbage (가비지) : 객체의 liftime > 바인딩의 lifetime
name이 a 객체와 binding 하고 있는 상황에서
해당 name에 다른 b 객체의 값을 지정해 주면,
a 객체는 사용되지 않는 값이 되는데 이를 가비지라고 한다. - Dangling Reference (허상 포인터) : 객체의 lifetime < 바인딩의 lifetime
4. 객체 생명주기의 분류
일반적으로, Object’s lifetime은 객체 공간 관리를 위한 3가지 메모리 할당 방법 중 하나와 일치한다.
정적 객체(Static object) : 프로그램 실행되는 동안 유지되는 가상(논리) 주소가 주어짐. // Heap과 Stack 사용
스택 객체(Stack object) : LIFO(Last-In, First-Out order), 재귀를 위한 함수 공간
힙 객체(Heap object) : 임의 시점에서 할당(사용자 재량), 동적 할당
-> 자세한 내용은 다음 글에서 다룬다.