brunch

You can make anything
by writing

C.S.Lewis

by 가필드의 인사이트 Oct 03. 2018

C++ 프로그램의 구조:: 존속기간(lifetime)

Standard C++ 정복

프로그램 내의 심볼은 자신이 생성되는 시점과 소멸되는 시점, 즉 존속기간(lifetime)을 가진다. 심볼이 생성된다는 것은 프로그램이 실행되어 메모리 공간을 할당 받는 것을 의미하고 소멸된다는 것은 공간을 반환하는 것이다. 이 존속기간을 설명하기 위해서는 프로그램의 실행 모델, 즉 프로세스 모델(process model)을 이해할 필요가 있다. 프로세스는 텍스트(text) 영역(또는 코드 영역), 데이터(data) 영역, 스택(stack), 자유공간(free store) 또는 힙(heap)으로 부르는 네 영역으로 이루어진다.                    


프로세스 모델


이 모델은 C 프로그램이든 C++ 프로그램이든 상관없이 컴파일-링크 과정을 거쳐 나온 실행 프로그램이 실행될 때 메모리 공간에 잡히는 일반적인 프로세스의 모델이다. 이 모델은 컴파일러와 사용 운영체제에 의존적인 개념적 모델이다. 대개 텍스트 영역의 주소가 낮으며 스택 영역의 주소가 높다. 또한 스택은 낮은 주소 쪽으로 자라난다.


텍스트 영역은 CPU에 의해 실행되는 명령어들이 기계어 형태로 들어 있는 고정된 크기의 읽기 전용 영역이다. 데이터 영역은 실행 시간 이전에 메모리 공간을 확보해야 하는 고정 크기의 읽기 및 쓰기 가능한 영역이다. 전역 변수나 정적 변수가 여기에 들어간다. 데이터 영역은 초기 값을 갖느냐의 여부에 따라 둘로 구분한다. 초기값을 갖지 않는 데이터 영역을 BBS(Block by Symbol Start)라 하고 프로그램의 실행이 시작될 때 영역이 할당되고 데이터는 자동 0으로 초기화된다. 초기값을 갖는 데이터는 컴파일 시간에 그 데이터에 대한 공간이 확보된다.


스택영역은 실행 시간에 시스템이 자동으로 필요에 따라 할당 및 반환하는 영역으로 자동 변수나 함수 호출에서의 복귀 주소와 함수 인자를 위해 할당된다. 자동 변수라는 것은 지역 변수이면서 타입 앞에 static이라는 키워드가 붙지 않은 변수를 말한다.


자유 공간은 사용자의 필요에 의해 실행 시간에 동적으로 할당 및 반환하는 영역이다.


존속기간이라는 것은 데이터가 프로세스 모델의 네 영역 중 어디에 잡히느냐는 것과 깊은 관계가 있다. 존속기간에는 프로세스 모델에서 텍스트 및 데이터 영역과 관련 있는 정적 존속기간(static lifetime), 스택 영역과 관련 있는 지역 존속기간(local lifetime), 그리고 자유 공간과 관련 있는 동적 존속기간(dynamic lifetime)의 세 가지가 있다.


정적 존속기간

정적 존속기간은 프로그램의 실행과 같은 일생, 즉 프로세스가 시작할 때 메모리 영역에 잡히고 프로세스가 끝날 때 없어지는 것으로 텍스트 영역과 데이터 영역에 잡히는 모든 심볼이 여기에 해당한다. 따라서 텍스트 영역에 잡히는 함수는 모두 이 존속기간을 갖는다. 또한 파일 범위를 갖는 변수나 객체들도 정적 존속기간을 가진다. 이들 변수나 객체는 데이터 영역에 잡히는 전역 심볼들이다. 지역 범위를 갖는 심볼 중에서도 데이터 영역에 잡히는 것이 있다. 내부 정적 심볼이 그것이다. 따라서 정적 변수나 정적 객체도 정적 존속기간을 갖는다.


지역 존속기간

지역 존속기간은 블록이 시작되면 심볼이 생성되고 블록이 끝날 때 소멸되는 것으로 스택에 잡히는 데이터가 이에 해당한다. 지역 존속기간을 갖는 심볼은 지역 범위를 가진다. 그러나 역은 성립하지 않는다. 내부 정적 변수와 같이 지역 범위를 갖지만 정적 존속기간을 가질 수 있기 때문이다. 범위와 존속기간은 밀접한 관계가 있지만 분명히 다른 개념이다. 또 스택에 잡히지 않으면서도 지역 존속기간을 갖는 변수가 있다. CPU에는 레지스터(register)라는 고속의 메모리가 있는데, 여기에 잡힐 수 있는 정수형 변수도 지역 존속기간을 가질 수 있다. 만일 할당할 레지스터 여유분이 없다면 스택에 잡히므로 일반적으로 지역 존속기간을 갖는 심볼은 스택에서 삶을 갖는다고 말할 수 있다.


동적 존속기간

자유 공간(free store)에 잡히는 제이터가 이에 해당하는 것으로 사용자에 의해 객체의 존속기간이 결정된다. C++는 C의 malloc() 함수와 같이 사용자가 실행 시간에 명시적으로 메모리 공간을 할당할 수 있는 연산자 new와 C의 free() 함수와 같이 사용자가 명시적으로 실행 시간에 메모리 공간을 반환하는 연산자 delete를 제공한다. 사용자가 new에 의해 자유공간에 할당한 객체는 사용자가 명시적으로 제거하지 않는 한 메모리 공간을 자리잡는다.


[프로그램 lifetime.cpp]를 통해서 개념을 구체적으로 알아보자.


예시 출력 결과


객체(1) 생성

        함수 main() 시작

객체(4) 생성

객체(5) 생성

        &main = 2036557

        &f    = 2036342

        &gobj = 2097952

        &mobj = 13630932

        &pobj = 13630920

        pobj  = 17711496

        함수 f() 시작

객체(2) 생성

객체(3) 생성

        &sobj = 2097960

        &fobj = 13630628

        함수 f() 끝

객체(3) 소멸

객체(5) 소멸

        함수 main() 끝

객체(4) 소멸

객체(2) 소멸

객체(1) 소멸



 lifetime.cpp 분석: 객체의 존속 기간

우선 객체가 언제 생성되고 언제 소멸되는지, 즉 존속기간에 초점을 두고 결과를 살펴보자. C++에서는 객체의 존속기간을 잘 확인할 수 있는데, 객체가 생성될 때 바로 생성자(constructor)가 호출되고, 소멸될 때는 소멸자(destructor)가 호출되기 때문이다. 8행은 생성자, 9행은 소멸자를 나타낸다. 12~16행에서 생성자를 정의하고 있고 18~21행에서 소멸자를 정의하고 있다.

프로그램 life.cpp 객체의 존속기간

gobj(1)은 전역 객체이므로 실행 프로그램이 시작될 때 데이터 영역에 잡혀서 프로그램이 실행되는 동안 계속 살아 있다가 프로그램이 끝날 때 같이 없어진다. 즉 프로그램의 실행과 같은 정적 존속기간을 갖는다. 프로그램의 결과를 보면 흥미로운 사실을 발견할 수 있는데, C나 C++ 프로그램의 경우 시작 진입점은 main() 함수임에도 불구하고 전역 객체를 생성할 때 불리는 생성자가 먼저 수행되고 있음을 알 수 있다. 이 객체는 가장 먼저 생성되고 가장 나중에 소멸된다.


정적 존속기간을 갖는 객체가 하나 더 있다. 함수 f()에서 선언된 정적 객체 sobj(2)가 그것이다. 그런데 이 객체는 함수 f()에 진입하고 나서야 생성자가 호출된다. 이것은 범위와 존속기간의 개념이 다르가는 것을 보여주는 예이다. 범위는 이 함수 블록에만 국한되는 지역 범위를 가지는 반명 존속기간은 프로그램의 실행과 동일한 정적 존속기간을 가지기 때문이다. 사실 이 정적 객체는 프로그램이 시작될 때 전역 객체와 같이 객체의 크기만큼 데이터 영역에 잡힌다. 그리고 함수 f()를 시작할 때 비로소 범위가 잡히기 때문에 그 때 생성자를 호출해서 멤버를 초기화한다. 이 객체는 함수 블록이 끝나도 곧바로 소멸되지 않고 프로그램이 종료될 때, 즉 main() 함수 블록이 끝날 때 전역 객체와 같이 소멸된다.


main() 함수 내의 지역 객체 mobj(4)와 함수 f()의 지역 객체 fobj(3)은 지역 존속기간을 가진다. 이들 지역 객체는 블록에서 선언될 때 생성되었다가 블록이 끝날 때 소멸된다.


마지막으로 사용자가 객체의 존속기간을 결정할 수 있는 예를 살펴보자. 39행의 의미는 C++ 동적 메모리 할당 연산자 new를 사용하여 클래스 C타입의 객체를 생성해서 이의 주소를 C타입의 포인터 변수 pobj에 대입하는 것이다. 물론 포인터 변수 pobj 자체는 스택에 잡히는 지역 존속기간을 갖는다. 즉 main() 함수가 끝나는 시점에서 스택에서 제거된다. 동적 존속기간을 갖는 것은 포인터 변수 pobj가 가리키는 내용. 즉 *pobj인 객체5이다. 객체5는 이렇게 생성되서 사용자가 명시적으로 소거하지 않는 한 자유 공간 영역에 자리잡게 된다. 이 객체를 소거하는 것은 전적으로 사용자 책임이다. 36행은 포인터 변수 pobj 자체를(스택 영역에서) 소거하라는 것이 아니라 pobj의 실제 참조 내용인 객체5를 자유공간 영역에서 소거하라는 의미이다. 여기에서 객체5의 운명이 끝나는 것이다.


lifetime.cpp 분석 : 객체의 프로세스 모델상의 위치

30, 31행과 40~45행에서 프로세스 모델 상에서 객체 위치에 대한 힌트를 제공한다. 우선 40행에서 main() 함수의 주소를 얻고 있다. &main은 main() 함수에 대한 주소를 나타내며 (unsigned long)(&main)은 이 주소를 unsigned long 타입으로 형변환을 시켜준다. 포인터 타입의 크기는 워드 크기이고 주소 공간은 양수이므로, unsigned long형으로 명시적 변환을 시켰다. main() 함수의 주소를 T라고 했을 때 T는 프로세스 모델 중에서 텍스트 영역의 주소 공간에 해당한다. 이 T값에 비교해서 함수 f()는 상대적으로 main() 함수와 가까운 위치에 있다(41행). 함수 f()도 텍스트 영역, 즉 실행 코드가 들어 있는 영역에 속하게 된다.                             


프로세스 모델 상의 객체 위치


42행에서는 전역 객체 gobj에 대한 주소를 얻고 있다. 이 주소를 D라 했을 때 D는 프로세스 모델 중에서 데이터 영역의 주소 공간에 해당한다. 결과를 보면 D는 T에 비해 상대적으로 높은 주소에 있음을 볼 수 있다. 데이터 영역에 잡히는 또 하나의 정적 객체 sobj는 D에 가깝게 위치하고 있다(30행).

스택에 잡히는 자동 변수나 자동 객체에는 main() 함수 블록 안의 지역 객체 mobj와 포인터 변수 pobj, 그리고 함수 f() 안의 지역 객체 fobj가 있다. 각각의 스택에서의 상대적인 위치는 컴파일러가 결정한다.

유일하게 자유 공간 영역, 즉 힙에 잡히는 객체5의 주소는 포인터 변수 pobj가 가지고 있다. 그 주소 값은 Windows 환경에서는 오히려 더 큰 값으로 나왔다. 이것은 원거리 힙(far heap)에 할당했기 때문이다. 원거리 힙은 개념적으로 스택 상단의 자유 공간 영역이다. 다른 컴파일러와 다른 환경에서는 다른 결과가 나올 수도 있다(UNIX 환경에서는 스택 주소보다 훨씬 작은 값에 할당 가능).

프로그래머는 각 객체가 메모리 상에 정확히 어디에 놓이는지 알 필요는 없다. 중요한 점은 객체에 존속기간을 다양하게 지정할 수 있다는 것이다. 즉 프로그램의 실행과 같은 존속기간을 주어 실행 중 필요한 때에 언제라도 그 객체를 접근하게 할 것인지(정적 존속기간) 아니면 계산의 중간 결과를 위해 임시 객체를 둘 것인지(지역 존속기간), 또는 객체의 생성과 소멸을 프로그래머가 제어 할 것인지(동적 존속기간)를 결정해야 한다. 이처럼 C++ 프로그래밍을 할 때는 항상 입체적인 시각에서 접근해야 하는 것이다.




작가의 이전글 C++ 프로그램의 구조:: 범위(Scope)
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari