이번에 C++을 다시 공부하면서 내가 작성한 코드가 실제로 어떻게 작동하는지 이해하는 것이 중요하다는 것을 알게 되었다. 그래서 컴퓨터 구조와 운영체제에 대해서 처음 공부를 시작하게 되었는데, 그 내용은 별개로 작성할 예정이고 이 글에서는 C++에서 사용되는 변수들이 메모리에서 어떻게 움직이는 것인지 정리해보았다. 변수의 특징이 메모리 공간과 밀접한 관련이 있기 때문에 메모리 영역에 대해서 우선 알아봐야 한다.
1. 스택 영역 (Stack)
2. 힙 영역 (Heap)
3. 데이터 영역 (Data)
4. 코드 영역 (Code, ROM; Read Only Memory)
이에 대해서는 나중에 컴퓨터 구조에서 메모리를 다룰 때 더 자세히 다룰 예정이고, 여기서는 간단하게만 작성하겠다.
스택 영역은 함수가 호출되었을 때 함수에서 정의된 변수들이 저장되는 공간이다. Call Stack이라고도 하며 함수가 호출될 때 스택의 특징에 따라 스택이 쌓이고, 함수가 종료될 때 스택이 빠진다. 따라서 함수가 종료될 때 함수 안에서 선언되었던 변수들이 스택이 빠지면서 같이 사라지게 된다. 함수가 너무 많이 호출되면 스택 영역에 더 이상 쌓을 수 없어 Stack Overflow라는 에러를 발생시킬 수 있어 유의해야 한다.
힙 영역은 사용자가 직접 메모리를 할당할 수 있는 메모리 영역으로, 주로 프로그램 run time에서 할당되어야 하는 데이터를 동적으로 할당하는 데 사용되는 공간이다. 이 게시글에서는 더 이상 언급되지 않을 예정이다.
데이터 영역은 프로그램이 실행되는 동안 계속 데이터가 유지되는 영역이다. 따라서 스택 영역과 다르게 중간에 사라지지 않아야 하거나 여러 함수에서 공용으로 접근할 수 있는 변수를 저장하는데 사용된다.
코드 영역은 우리가 작성한 코드가 기계어 명령어의 형태로 저장되는 공간으로, 빌드 이후에 변경될 수 없는 읽기 전용 메모리이다. 추가적으로, 코드에서 작성한 문자열이나 리터럴상수도 같이 저장된다.
C++에서 사용되는 변수는 4가지로 나뉜다.
1. 지역 변수 (Local Variables)
2. 전역 변수 (Global Variables)
3. 정적 변수 (Static Variables)
4. 외부 변수 (External Variables)
1. 지역 변수
지역 변수는 지역, 특히 함수 안에서 선언된 변수를 의미한다. 지역변수는 스택영역에 저장되며, 함수가 종료될 때 사라지는 변수이다.
void func ()
{
int local = 10;
return;
}
int main ()
{
int local = 0;
func ();
return 0;
}
위에 두 가지 함수가 있는데, 각 함수마다 서로 다른 지역이므로 동일한 변수명을 사용할 수 있고, 서로 간에 변수명으로 직접적인 접근은 불가능하다. 위 코드는 main 함수에서 func()을 호출했다가 아무것도 하지 않는 local이라는 변수를 선언했다가 종료된다. 아래에 그 과정을 거치면서 스택 메모리에 생기는 일을 나타낸 것이다. 중요한 것은 스택에 메모리를 할당할 때, 메모리를 얼마나 할당해야 하는 지 알아야 하기 때문에 함수 안에서 사용하는 모든 지역변수의 크기를 컴파일 과정에서 알아야 한다. 그리고 그 결과로 스택영역에 메모리가 적재된다. 위의 경우 각각 함수가 int형 데이터 1개씩을 선언했기 때문에 stack의 크기가 4byte이다. 스택영역에는 오로지 함수에서 사용하는 지역변수만 적재되고, 코드 (명령어)는 코드 영역에 저장된다는 점을 간과하기 쉽다.
2. 전역 변수
전역 변수는 함수 바깥에서 선언된 변수로, 지역변수와 다르게 데이터 영역에 적재된다. 데이터 영역에 적재되기 때문에 프로그램 실행 도중에 사라지지 않고 유지되며, 다른 함수에서 모두 접근할 수 있다. 또한 이러한 특징 때문에 동일한 변수명을 또 선언할 수 없다. 그리고 데이터 영역에 저장되는 변수들을 이해하기 위해서는 프로젝트의 빌드 과정에 대해 좀 더 알아야 한다.
C++ 프로젝트는 우선 파일별로 컴파일 단계를 거친 뒤 목적파일을 생성한다. 그 후 모든 파일의 명령어를 합쳐서 수행하기 위해 링킹 단계를 거쳐 실행파일이 만들어진다. 링킹단계는 "전방 선언"이라는 시스템을 사용하기 위해 필요한 과정이다. 전방 선언은 어떤 함수를 사용하기 전에 그 함수가 있다는 사실을 알리기 위해 함수의 형태만 미리 알려주는 것이다. 실제 함수 구현은 다른 곳에 되어있으므로 당장 그 내용은 알 수 없지만 컴파일 시 문법오류로 인식하지 않는다. 컴파일러는 컴파일 단계에서 함수가 있다는 사실만 알고 일단 단계를 넘어갈 수 있게 되는 것이다. 이러한 전방선언된 부분과 실제 구현 부분을 연결해 주는 것이 링킹 단계라고 이해하면 된다.
이 때 링킹 단계에서 모든 파일의 내용을 하나로 통합하기 때문에 만약 파일별로 동일한 변수명의 전역변수를 선언했다면, 파일별로 실행하는 컴파일 단계에서는 오류가 나지 않지만 링킹단계에서 변수가 중복선언되었다는 오류를 검출하게 된다.
그렇다면 링킹단계에서 파일의 내용이 하나로 통합되니까 다른 파일에서 선언된 전역변수를 사용할 수 있다고 생각할 수도 있는데, 이것은 애초에 파일별로 컴파일 단계를 거칠 때 자신의 파일에 선언되어 있지 않으면 사용할 수 없기 때문에 컴파일 에러가 난다. 이런 것을 하고 싶으면 후술할 외부 변수를 사용하는 방법이 있다.
# main.cpp
int global = 0;
void func()
{
global = 10;
}
int main()
{
func(); // global == 10
global += 10; // global == 20
return 0;
}
위의 global 변수처럼 함수 바깥에 선언된 것이 전역변수이고, 모든 함수에서 접근하고 공유하는 변수이다. func()에서 수정한 내용도, main()에서 수정한 내용도 모두 반영되는 것을 확인할 수 있다. 다만 이렇게 모든 곳에서 접근할 수 있다보니 의도치 않은 일이 발생할 수 있다는 점을 주의해야 한다.
// other.cpp
// int global = 10; // 링크 에러. 링킹 단계에서 전역변수 global이 중복 정의된 것을 확인한다.
int func_other()
{
// global += 10; // 컴파일 에러. 다른 파일에 선언한 전역변수에 접근할 수 없다.
}
3. 정적 변수
정적 변수는 선언된 곳에서만 접근 가능한 변수로, 데이터 영역에 저장된다. 선언된 곳이라 함은 함수 내부 또는 cpp 파일로 이해할 수 있다.
void count()
{
static int staticInt = 0;
++staticInt;
return;
}
int main ()
{
count(); // 1
count(); // 2
count(); // 3
return 0;
}
위와 같이 count() 안에서 선언된 static 변수 staticInt는 count() 안에서만 프로그램 실행 중에 계속 살아있는 변수이다. count()가 호출될 때마다 0으로 초기화되는 것 아니냐고 생각할 수 있지만, static 변수는 선언시 최초 1회만 초기화되고, 이후에는 실행되지 않는다. 따라서 일반적인 지역변수라면 count() 종료시에 사라지지만 정적변수 staticInt는 프로그램 run time 중에는 계속 유지되면서 count()를 실행할 때마다 1씩 증가하게 된다.
전역변수 위치에 정적변수를 선언하면, 한 파일 내에서는 전역변수와 동일하게 기능하지만 여러 파일을 빌드했을 때 중복정의 하면 링킹에러가 발생하는 전역변수와 달리 선언된 파일에서만 사용 가능한 정적 변수는 동일한 변수명을 다른 파일에서도 선언할 수 있다.
// main.cpp
static int staticInt = 0;
int main()
{
staticInt = 10;
return 0;
}
// other.cpp
static int staticInt = 0;
int func_other()
{
staticInt = 20;
return 0;
}
이 때 변수 명은 동일하지만 서로 다른 파일에 선언되어 있으므로 실제로는 다른 메모리 주소를 참조하고 있으며, 공유되지 않는다. main.cpp의 staticInt는 10, other.cpp의 staticInt 는 20이다.
4. 외부 변수
외부 변수는 다른 파일에서 선언된 전역변수를 사용할 수 있도록 가져온 변수이다. 내가 느끼기에 다른 변수와는 약간 결이 다른데, extern이라는 키워드가 변수를 선언한다는 느낌보다는 가져온다는 느낌이다. 함수의 전방선언과 비슷하다는 해석도 있다.
// main.cpp
extern int ExternInt;
int main()
{
ExternInt = 10;
return 0;
}
// other.cpp
int ExternInt = 0;
main.cpp에서 other.cpp에 선언된 ExternInt를 가져다 쓰는 코드이다. 일단 ExternInt라는 변수가 있다는 것만 알려두고, 링킹과정에서 다른 파일에서 그 변수를 찾아서 사용하는, 함수 전방선언과 비슷한 과정을 거친다. 이를 이용해서 어떤 cpp 파일에 여러 파일에서 공유하고 싶은 변수를 선언해두고, 헤더 파일에 extern으로 그 변수를 선언하는 구문을 작성해서 그 헤더파일을 include 한 모든 파일들이 그 변수에 접근할 수 있도록 할 수 있다.
Static과 Extern 변수가 처음 봤을 때 헷갈리고 좀 생소했으나 메모리와 연관지어 생각하다보면 쉽게 이해할 수 있게 된다.
[C++] 함수 객체 (Functor) (0) | 2024.08.08 |
---|---|
[C++] 함수 포인터 (0) | 2024.03.19 |
[C++] 포인터 (0) | 2024.03.19 |
[C++] Struct (구조체) (0) | 2024.03.12 |
[C++] 자료형 (Data Type) (0) | 2024.02.29 |