반응형

우리는 전역(global) 변수와 정적(static) 변수를 혼용해서 쓰는 경우가 많다.

어느 정도 차이가 있다는것은 알지만 그 디테일에 대해서는 잘 모르는데,

이번 포스팅에서는 각 변수의 디테일한 특징과 차이에 대해 설명해 보도록 하자.







1. 전역 (global) 변수


라이프 타임은 프로그램이 죽을때까지이며, 다른 파일에서도 해당 변수에 접근이 가능하다.

scope가 없는 곳에서 "int g_value" 이런 식으로 선언한 변수는 전역 변수가 된다.

막상 쓰려고 할때 자주 헷갈리는 부분인데 이 변수를 다른 파일에서 사용하기 위해서는

"extern int g_value" 이런식으로 선언하고 사용해야한다. 이말은 int g_value가 다른 파일 어딘가에 있다는 것을 나타낸다.

정리하자면 본문에서는 "int g_value", 다른 파일에서는 "extern int g_value"이런 식으로 선언하여 사용해야 한다는것.








2. 정적 (static) 변수


라이프 타임은 전역변수와 마찬가지로 프로그램이 죽을때까지이다.

static이라는 키워드가 반드시 붙으며, 해당 변수가 선언된 scope에 따라서 접근 가능한 범위가 결정된다.

"static int s_value"

위와 같이 선언되며 다른 파일에서는 접근 할 수 없으며 오직 해당 파일에서만 scope에 맞게 접근이 가능하다.


흔히 비지역 정적 객체, 지역 정적 객체라는 헷갈리는 말을 많이 쓰는데, 이 말만 보면 비지역 정적 객체는 scope 없는 곳에서 만든 static int s_value같은 느낌이지만 그냥 지역적으로 만들어 지지 않은 전역 변수 + 정적 변수를 통칭한다. (Effective C++ 번역의 폐혜인듯..)








3. 전역 변수와 정적 변수의 메모리 할당 위치




위 메모리 맵을 보면 위쪽에 .data영역과 .bss영역이 있다.


.bss 영역은 초기화 되지 않은 전역변수 및 정적변수가 존재하는 공간으로 모든 값은 0으로 초기화되며

.data 영역은 초기화 된 전역변수 및 정적변수가 존재하는 공간이다. 하지만 수식이나 함수에 의해 초기화 된 변수일 경우, .data가 아닌 .bss 영역으로 들어가게 된다.



다음 각각의 경우를 통해 실제로 어떻게 메모리에 올라가는지 살펴보자



해당 코드를 컴파일하고, 어셈블리 코드를 까보면 다음과 같이 나오는걸 볼 수 있다.


보는것 처럼 초기화를 한 변수는 _DATA 영역에 메모리가 잡히며, 초기화 하지 않은 변수는 _BSS영역에 잡히는 것을 볼 수 있다.

하지만 static 변수의 경우, 그냥 global 변수와는 다르게 초기화를 하지 않은 상태에서 해당 변수를 본문 어디에서도 사용하지 않으면 메모리 자체가 잡히지 않는걸 확인 할 수 있었다.

반면 global 변수는 초기화를 하지 않더라도 무조건 메모리에 올라간다.

참고로 위 예제에서는 s_value2 변수를 본문에서 사용하고 있기때문에 메모리에 올라간 것이다.









4. 전역 변수와 정적 변수의 초기화 시점




① 정적 '객체'의 초기화

위 코드를 보면 static 변수와 static 객체를 지역적으로 선언하고 초기화 하고 있다.

그렇다면 초기화를 했기 때문에 두 값다 _DATA영역에 올라갈 것이고, 값의 초기화는 메모리에 올라감과 동시에 진행될 것인가?




어셈파일을 까본 결과 일반 변수의 대입 초기화 같은 경우 예상대로 _DATA영역에 올라가고 값도 초기화 되지만,

클래스 객체의 경우 _BSS영역에 올라가게 된다.

이렇게 _BSS영역에 올라간 객체는 대입이 이라면 dynamic initializer가 진행되고, 생성자를 통한 초기화라면 해당 변수가 처음 수행되는 시점에 초기화가 이루어진다. (dynamic initializer에 대해선 뒤에서..)





객체의 초기화를 디스어셈블로 지켜보면, 처음 해당 변수의 선언부가 수행되는 시점에 eax 레지스터의 값을 플레그로하여 값이 0이면 생성자 구문을 수행하고, 1이면 해당 생성자 코드는 점프해 버린다.


우리가 프로파일러를 만들때 static 객체를 이용하는 경우가 있는데, 저런식으로 scope안에 객체를 만들고 생성자를 통해 초기화를 진행하면 메모리는 처음부터 올라가 있지만 해당 생성자 구문은 처음 수행되는 시점에 한 번만 호출되는 것이다.

(루프문을 다시 돌아와 static Test class_value(10); 구문을 지나도 수행하지 않고 점프해 버린다는 뜻이다)





② dynamic initializer (동적 초기화)

그렇다면 위에서 말한 dynamic initializer란 무엇일까?

몇가지 예제 코드를 보자.


위 코드의 경우 컴파일 시점에서 functionValue라는 변수의 값을 알아 낼 수 없기 때문에 해당 변수는 _BSS영역에 메모리만 올리고(0으로도 초기화 될것이다) 추후에 동적으로 초기화가 수행된다.

main()함수가 실행되기 전에 해당 변수는 dynamic initializer를 수행하게 되고,

main 구문을 수행하고 있을때는 이미 10이란 값으로 초기화가 되어 있을것이다.









또다른 예를 보자.


다른 파일에 global 변수를 선언한다.



main 파일에서는 해당 변수를 extern으로 얻어다가,

static int s_value에 더한다. 이때 s_value값은 무엇으로 채워질것인가?



위 함수 예제와 마찬가지로 _BSS영역에 먼저 메모리를 올려 놓고 dynamic initializer를 수행한다.

다른 파일에 있는 global변수를 extern으로 선언하여 가져다가 사용할 때, 해당 변수의 값을 알수 있는 시점은 컴파일 시점이 아닌 링크 시점이다.


이를 번역단위라고 하는데, 각 파일은 각각의 번역단위라 할 수 있고 이 번역단위들은 링크시점에 하나의 실행파일로 묶이게 된다. 즉, 컴파일 시점에 알 수 없는 값이므로 임시로 메모리만 올려 놓고 dynamic initializer를 통해 main() 함수 전에 수행되는것이다. 


이와 관련해서 Effective C++에서는 다른 번역단위에서 사용하는 global 변수들의 초기화 순서가 정해져 있지 않음을 지적하고 있다.

번역단위가 여러개인 시점에서 위의 g_value1을 사용하는 다른 파일이 또 있고, g_value가 아직 링크되기 전이라면 0값으로 쓰여져 어떤 파일에서 g_value1 변수를 사용하는 부분이 있다면 문제의 소지가 생긴다는 뜻이다.


그래서 저자는 싱글톤처럼 초기화 시점을 조정할 수 있도록 만들기를 권고한다.(디테일은 생략)








5. 전역 변수와 정적 변수의 차이 정리


① 전역(global) 변수는 다른 파일에서도 가져다 쓸수 있지만 정적(static) 변수는 해당 파일의 scope안에서만 접근이 가능하다.


② 초기화 하지 않은 정적(static) 변수의 경우 본문에서 사용하지 않으면 아예 메모리 상에 올라오지 않는다.


③ 정적(static) 객체의 경우 처음 구문이 수행되는 시점에 처음 생성자를 호출하도록 할 수 있다.. 이를 함수화하여 호출을하면 생성자의 호출 시점을 조정하는게 가능해진다.




[참고]

https://www.tumblr.com/search/%EC%A0%95%EC%A0%81    // Effective C++ 잘 해설.

https://kldp.org/files/static____________160.htm               // static에 대한 설명

http://pangate.com/541                                       // extern과 static

https://kldp.org/node/122255                                 // bss의 용도

http://stackoverflow.com/questions/5945897/what-is-dynamic-intialization-of-object-in-c // 스택 오버플로우, 동적 초기화



ref : http://chfhrqnfrhc.tistory.com/entry/전역변수와-전적변수

반응형

+ Recent posts