코딩을 하면서 static변수를 쓰면서 최근 문득 든 생각이 있다. “static변수를 생성자에서 초기화하면 새로운 객체를 로드할 때마다 해당 값으로 덮어써지는거 아니야?” 라는 생각이었다.
물론 턱도 없는 생각이라는 걸 잘 알고 있고 그렇게 작동하지 않는다는 것쯤은 직관적으로 이해하고 있었지만 실제로 이가 어떻게 작동하는지가 궁금해 간단한 예제를 짜보았다.
#include <iostream>
using namespace std;
class MyClass{
public:
MyClass(){
static int a = 0; // 1번
a++; // 2번
cout<<a<<endl; // 3번
}
};
int main()
{
cout<<"Hello World"<<endl;;
MyClass();
MyClass();
MyClass();
MyClass();
MyClass();
return 0;
}
1
2
3
4
5
그 결과는 당연하게도 초기화는 한 번 만 일어난다는 것이었다. 그렇다면 드는 자연스러운 의문, 대체 초기화는 언제 하는건데? 그에 대한 해답은 아래 링크에 나와있다.
(아주 만족스럽게 설명이 잘 돼있다)
C++ 프로그래밍 :정적 지역 변수(Static Local Variable) 원리
.
.
.
.
.
.
라고 끝내기엔 너무 무책임하니 그 이유를 간단하게 요약해보도록 하겠다.
static변수라 함은 함수 호출시 스택에 쌓이는 형태로 동작하는 것이 아닌 전역 영역에 그 데이터가 저장되는 변수라는 것을 다들 알 것이다. 그렇기 때문에 전역 변수를 포함해서 이러한 정적 지역 변수들의 초기화, 생성 시점은 코드가 삽입돼있는 함수가 실행될 때가 아닌 프로세스의 시작 부분이라는 것이다.
때문에, 위처럼 생성자 내부에 초기화 코드를 박아놨어도 해당 변수는 프로세스의 시작에 초기화된다. 생성은 이미 돼있고 접근만 해당 스택 프레임에서 가능하다는 것이다.
실제로 컴파일을 하면 주석으로 적어놓은 2번
, 3번
만 어셈블리의 함수 호출부에 임베딩되고 1번
에 해당하는 어셈블리 명령어는 찾아볼 수 없다.
static int a = 0; // 대응하는 어셈블리 코드가 없다.
a++;
00007FF7D2F722DF mov eax,dword ptr [a (07FF7D2F83440h)]
00007FF7D2F722E5 inc eax
00007FF7D2F722E7 mov dword ptr [a (07FF7D2F83440h)],eax
실제 위 코드는 Visual Studio의 디스어셈블리 창인데 static int a = 0;
에 해당하는 어셈블리 코드가 없는걸 볼 수 있다.
int ret2() {
return 2;
}
void staticExam() {
static int a = ret2();
a++;
}
그러면 위와 같은 코드는 어떻게 동작할까? 상수의 초기화를 함수를 이용하고 있다.
이 말인 즉, 내가 MyFunc1()에 도달하기 전 모든 데이터가 이미 스택프레임에 올라가고 나서야 초기화가 가능하다는 것이고 위 스태틱 변수는 초기화 시점이 프로세스의 시작부분이 될 수 없다는 것이다.
이에 대한 대답은 해당 코드를 디어셈블리 해보면 알 수 있다.
static int a = ret2();
00007FF77B641DAB mov eax,104h
00007FF77B641DB0 mov eax,eax
00007FF77B641DB2 mov ecx,dword ptr [_tls_index (07FF77B6524B0h)]
00007FF77B641DB8 mov rdx,qword ptr gs:[58h]
00007FF77B641DC1 mov rcx,qword ptr [rdx+rcx*8]
00007FF77B641DC5 mov eax,dword ptr [rax+rcx]
00007FF77B641DC8 cmp dword ptr [$TSS0 (07FF77B652450h)],eax ; cmp 실행
00007FF77B641DCE jle staticExam+6Ch (07FF77B641DFCh) ; jle 실행
00007FF77B641DD0 lea rcx,[$TSS0 (07FF77B652450h)]
00007FF77B641DD7 call _Init_thread_header (07FF77B641528h)
00007FF77B641DDC cmp dword ptr [$TSS0 (07FF77B652450h)],0FFFFFFFFh
00007FF77B641DE3 jne staticExam+6Ch (07FF77B641DFCh) ; cmp, jne 실행
00007FF77B641DE5 call ret2 (07FF77B641442h)
00007FF77B641DEA mov dword ptr [a (07FF77B65244Ch)],eax
00007FF77B641DF0 lea rcx,[$TSS0 (07FF77B652450h)]
00007FF77B641DF7 call _Init_thread_footer (07FF77B641541h)
a++;
00007FF77B641DFC mov eax,dword ptr [a (07FF77B65244Ch)] ; 점프하고자하는 주소
00007FF77B641E02 inc eax
00007FF77B641E04 mov dword ptr [a (07FF77B65244Ch)],eax
_Init_thread_header - threadSafeInit (_Init_thread_header가 어떻게 작동하는지 궁금하면 참조)
위 어셈블리 코드를 보도록하자. 사실 안봐도 상관 없다. (사실 나도 잘 모른다)
그저 코드에 cmp
jle
jne
명령어가 있다는 것만 보면 된다.
그게 뭐하는 명령어이냐? 바로 if문
이라고 보면 된다.
즉 컴파일러는 자체적으로 해당 함수가 초기화됐는지를 if문
으로 비교하여 처음에만 함수가 초기화 될 수 있도록 해준다고 보면 된다.