본문 바로가기
연구소👨‍💻/CS연구소

[C++] 메모리 구조

by 신그자체김상범 2024. 5. 30.

왜 메모리구조 얘길하나

메모리 구조는 코딩 시작한 사람들이면 대부분 한 번씩은 보고 넘어가게된다. 힙메모리, 스택메모리 같은 것들은 반드시 들어봤을 것이다. 그런데 이게 막 배우기 시작한 입장에서는 감이 잘잡히지 않는 개념들이다. 일단 코딩과 직접적으로 연관되어있지않고, 언어와 OS가 섞인 개념이라고 해야하기 때문에 그렇다.

하지만 그렇다고 메모리 구조를 공부하지 않을 수는 없다. 메모리 모델은 언어에 대한 심도있는 이해에 반드시 필요하기 때문에, 그런 의미에서 이번에 이해못했던 개념들을 한번에 설명하는 시간을 갖기로 했다.

종류

일단 C++을 기준으로는 메모리 구조가 다음과 같다.
이외 고려해야할 메모리들, 캐시 메모리, 가상 메모리
꽤나 많은 종류가 존재한다. 각파트의 정의는 다음처럼 설명할 수 있다.

  1. 코드 세그먼트
  2. 데이터 세그먼트
  3. 스택 세그먼트
  4. 힙 세그먼트
  5. 메모리 매핑 세그먼트

이외에도 가상 메모리, 캐싱 메모리들을 신경써야 한다고 한다.

이 중에서 코드 세그먼트는 함수나 클래스, 작은 코드조각들을 기계어로 바꾸고 그걸 매핑해주는 메모리이다, 함수를 호출할때마다 그 구문을 읽는 것이 아니라 미리 저장해놓은 코드를 쓰게 하기 위함이다. 그러다보니 우리가 직접 코딩에서 호출하거나 할 일은 없고, 한 번 프로그램이 실행됐을땐 수정할 수도 없다.

또 메모리 매핑 세그먼트는 파일, 장치에 매핑된 메모리 영역으로 좀 더 넓은 범위를 다룬다. 그래서 이번에는 다루지 않으려한다.

힙 세그먼트와 스택 세그먼트, 데이터 세그먼트

처음 메모리 구조를 공부한다면 아마 다들 힙 세그먼트. 스택 세그먼트를 먼저 보게 될 것인데. 초심자 입장에서 이해할 수 있는 부분은 힙 세그먼트에서는 동적으로 메모리를 할당할 때 사용되고 즉 new, delete나 스마트 포인터를 사용할 때 선언된 변수가 저장되는 곳이라고 정리할 수 있다.
그러면 이제 자연스럽게 스택 세그먼트는 정적으로 선언되는 변수들 말 그대로

int a = 3;

이런 변수들이 저장되는 곳이라고 생각하기 쉬워진다.
하지만 실제로 저런 방식으로 시작과 함께 선언된 데이터들은 데이터 세그먼트에 저장된다. 여기서 중요한건 '시작과 함께' 라는 말은 C++에서는 main함수 밖에 있는, 어떤 스코프 '{}' 안에도 포함되지 않는 변수들을 의미한다.

그럼 스택 세그먼트에는 뭐가 저장되냐? 스택에서는 함수가 호출 될 때 그 안에있는 지역변수나 매개변수들이 저장된다. 스택 구조는 재귀 구조를 생각하면 쉬운데 함수 a, b, c 가 순서대로 호출되면 보통 가장 먼저 끝나서 리턴값을 뱉는 함수는 c다. 즉 이런 재귀의 구조가 LIFO인 것이 스택 구조와 관련이 있는게 아닐까 생각한다. 그리고 C++에서는 main함수 역시도 함수로 친다 그래서 main 함수 안에 있는 변수들도 스택 구조에 들어간다.

만약 스택 세그먼트에서 데이터 세그먼트 변수에 접근해야 한다면?

변수가 함수 스코프 내에서 선언되고 초기화 된다면 이 방식은 직관적으로 보이지만 함수를 쓰다보면 전역변수를 써서 문제를 푸는 경우가 있는데 이럴때는 그 전역변수의 주소값을 포인터화 해서 컴파일러가 자동으로 스택 메모리에 합쳐준다고 한다. GPT에 따르면 이 때 3가지 작업을 한다고 한다.

1.    주소 계산: 데이터 세그먼트에 저장된 변수의 주소를 계산합니다.
2.    주소 전달: 함수 호출 시 해당 주소를 매개변수로 전달합니다.
3.    포인터 사용: 함수 내에서 포인터를 통해 해당 변수에 접근합니다.

스택 세그먼트와 static

스택 세그먼트의 정의대로면 함수가 리턴될 때 변수는 자동으로 스택메모리에서 전부 사라진다. 물론 별문제가 아닐 때도 있지만 함수에 따라서 그 값을 영구적으로 저장해 놓아야 할 때도 많다. 전역변수를 사용해서 문제를 해결할 수는 있지만 그러면 코드가 객체지향적이지 않아진다.
이럴 때 사용할 수 있는 것이 static이라는 변수인데.

#include <iostream>

class MyClass {
public:
    static int sharedVar;  // 클래스 레벨에서 공유되는 정적 변수

    void display() {
        std::cout << "sharedVar: " << sharedVar << std::endl;
    }
};

int MyClass::sharedVar = 0;  // 정적 변수 초기화

int main() {
    MyClass obj1, obj2;

    MyClass::sharedVar = 100;  // 정적 변수에 값 할당

    obj1.display();  // sharedVar: 100
    obj2.display();  // sharedVar: 100

    return 0;
}

함수나 클래스에서 static을 쓰게 된다면 그 변수는 스택 세그먼트가 아니라 데이터 세그먼트에 저장된다. 그리고 단 한번만 초기화된다.
또 하나 중요한 특징은 static 변수는 값이 변경되면 그것이 유지되기 때문에 전역 변수를 가져오는 것과 같은 역할을 하게된다.

파이썬을 써봤다면 이 코드에서 생각나는게 인스턴스 변수와 클래스 변수일 것이다. 파이썬에서는 인스턴스마다 다른 고유한 값을 같게되는 변수가 있고 모든 인스턴스에 공유되는 값이 있는데 static과 같은 성질이다.

class MyClass:
    shared_var = 0  # 클래스 레벨에서 공유되는 클래스 변수

    def display(self):
        print(f'shared_var: {MyClass.shared_var}')

obj1 = MyClass()
obj2 = MyClass()

MyClass.shared_var = 100  # 클래스 변수에 값 할당

obj1.display()  # shared_var: 100
obj2.display()  # shared_var: 100

그리고 이 static 변수는 java언어에서도 같은 의미로 사용된다. 그래서 어떤 변수를 함수 컴포넌트 안에서 선언하고 그 값이 변경될 때 값을 유지하고 싶다면 static을 사용해서 문제를 해결할 수 있다.

스택, 힙 세그먼트와 메모리 누수

여기까지 공부했으면 이런 생각을 해볼 수 있다.

함수 내에서 new로 데이터를 만들면 어디에 저장되지?

당연히 new를 사용했기 때문에 그 선언된 데이터는 힙 세그먼트에 저장된다 그런데 그때 함수가 return 해버리면? 힙 메모리에 접근할 수 있는 변수는 사라져 버리기 때문에 이제 이 힙 메모리는 사용할 수 없는 것이 된다. 이게 바로 C++에서 흔히 듣는 메모리 누수다. 메모리 누수 자체만 들으면 시각적으로 와닿지 않지만 데이터 구조를 같이 공부하면 좀 더 명확하게 그 원인을 알 수 있다.

 

 

 

이렇게 C++의 메모리구조에 대해서 공부를 해보았고 하는김에 언어를 쓰다보면 자주 만나는 static이라는 선언자에 대해서도 그 목적과 필요성을 한번에 알 수 있었다. 정리하자면

 

 

스택 세그먼트 : 함수 호출시 데이터를 LIFO 방식으로 저장하고 매개변수, 지역변수, 반환 주소를 저장한다.
힙 세그먼트 : new 와 delete, 스마트 포인터등으로 선언된 데이터를 저장하는 세그먼트
데이터 세그먼트 : static 변수나 main함수 바깥의 데이터들을 저장하는 곳
코드 세그먼트 : 함수, 클래스등의 데이터들이 기계어로 번역된 데이터들을 저장해 놓는 곳

 

일단 여기까지 공부하고 나머지 메모리 매핑 세그먼트는 나중에 입출력을 공부할 때 같이 해야겠다.