C++

[C++] 자원 관리하기

programmer-faust 2025. 6. 9. 20:04
  • 순서
    1. 스택 메모리
    2. 힙(동적) 메모리
    3. Dangling Pointer
    4. Memory Leak
    5. 스마트 포인터
    6. 얕은 복사와 깊은 복사
    7. 언리얼 엔진의 메모리 관리
  • 스택 메모리
    1. 함수 내에서 선언된 지역 변수와 매개변수는 별도로 동적 할당하지 않는 한, 기본적으로 스택 메모리에 저장되며, 함수가 끝나면 자동으로 해제됨.
    2. 예외적인 상황으로 함수 내의 제어구조, 블록, 스코프 등에서 선언된 변수는 해당 지역이 끝날 때 마다 소멸됨.
    3. 일반적으로 할당 가능한 스택 메모리의 크기가 제한적임
    4. 변수의 스코프(생존 영역)을 벗어나면 자동으로 해제되므로, 메모리를 더 길거나 유연하게 관리하기 어려움 => 해당 문제를 해결하기 위해 힙(동적)메모리를 사용할 수 있음
  • 힙 메모리
    1. 동적 할당 시 new 연산자를 사용하고, 해제 시 delete 연산자를 사용함
    2. 스택과 달리 자동으로 해제되지 않으므로 메모리 누수 등의 위험이 있을 수 있음
    3. 동적 할당된 객체 또는 변수의 생존 주기는 사용자가 delete로 해제할 때까지 유지됨.
  • Dangling Pointer
    1. 더 이상 유효하지 않은(이미 해제된) 메모리를 가리키고 있는 포인터
    2. 포인터는 메모리가 해제되었는지 여부를 자동으로 알 수 없기 때문에 발생하는 문제.
    3. 해당 상태에서 포인터를 역참조 하게되면 예측 불가한 문제가 발생할 수 있음.
  • Memory Leak
    1. 메모리 누수현상.
    2. 동적으로 할당한 메모리를 사용한 후 제대로 해제하지 않으면, 계속해서 사용하지 않는 메모리가 쌓이게 되는데, 프로그램이 점점 더 많은 메모리를 차지하게 되어 결국 사용할 수 있는 메모리가 부족해지게 된다. 해당 현상을 메모리 누수현상 Memory Leak이라고 한다.
  • 스마트 포인터
    1. unique_ptr(유니크 포인터)
      • 단일 소유권을 관리함
      • 객체에 대한 소유권을 다른 포인터에 이전할 수 있지만 여러개의 포인터가 동일한 객체를 소유할 수 없다.
      • 소유권의 개념만 있기 때문에 복사 혹은 대입이 불가능함.
      • 복사가 불가능하여 move를 사용해서 소유권 이전만 가능함.
    2. shared_ptr(쉐어드 포인터)
      • 내부적으로 레퍼런스 카운트를 관리함 - 레퍼런스 카운트는 현재 객체를 참조하는 포인터의 개수를 카운팅 하는 것
      • 여러개의 포인터로 하나의 객체를 소유할 수 있다.
      • 레퍼런스 카운트가 0이 되면 자동으로 메모리가 해제되어 DanglingPointer나 Memory Leak 문제를 효과적으로 방지 가능하다.
      • use_count() 메서드를 활용하여 현재 객체를 참조하는 포인터의 수를 확인할 수 있음.
      • reset() 메서드로 소유 중인 객체를 해제하거나 다른 객체로 변경할 수 있음.
    3. weak_ptr( 포인터)
      • 객체의 소유권을 공유하지 않음.
      • 레퍼런스 카운트를 증가시키지 않는 약한 참조를 하기 때문에 lock() 호출 후 반환된 shared_ptr이 유효한지 확인 후에 사용해야함.
      • weak_ptr은 레퍼런스 카운트에 영향을 미치지 않기 때문에 반드시 lock()함수로 내부 객체 유효성을 확인하고 사용해야한다.
      • 순환참조가 발생함
        • 순환참조: 두 개 이상의 객체가 서로를 shared_ptr로 가리켜 참조하는 상황 => 메모리 누수가 발생할 수 있음. 서로가 서로를 가리키고 있기 때문에 레퍼런스 카운트가 0이 안될 수 있기 때문
        • 순환참조가 발생하지 않도록 순환하고 있는 shared_ptr중 하나를 weak_ptr로 대체하면 순환고리가 끊어져 문제를 해결할 수 있음
      • shared_ptr은 관찰과 소유를 하는 반면 weak_ptr은 관찰만 한다고 표현함.
  • 얕은 복사와 깊은 복사
    1. 일반적으로 포인터나 동적으로 할당된 자원을 관리하는 객체는 메모리 안정성을 위해 깊은 복사를 사용하는 것이 바람직함.
    2. 얕은 복사
      • 클래스 내의 포인터 멤버를 복사할 때 포인터가 가리키는 데이터가 아닌 포인터가 저장하고 있는 주소값만 복사하는 것을 의미함
      • 두 객체가 동일한 동적 메모리 영역을 가리키게 됨. 얕은복사를 수행한 후 원본 객체가 메모리를 해제하면, 복사된 객체의 포인터는 해제된 메모리 영역을 카리게 되므로 dangling pointer가 발생할 수 있음.
    3. 깊은 복사
      • 클래스의 포인터 멤버가 가리키는 동적 데이터를 새로 할당된 독립적은 메모리 영역에 복제하는 것을 의미함.
      • 원본 객체와 복사된 객체는 서로 독립적인 메모리 공간을 소유하므로 dangling pointer가 발생하지 않음.
  • 언리얼엔진의 메모리 관리
    1. 가비지 컬렉션(GC)
      • 가비지 컬렉션은 언리얼 엔진에서 객체들의 메모리 관리를 자동화하기 위해 사용함
      • 장점: 개발자가 메모리 해제를 수동으로 처리하는 부담을 덜고, 메모리 누수나 dangling pointer와 같은 메모리 오류를 줄일 수 있음
      • 마크 앤 스윕 알고리즘 방식으로 동작함 - 주기적으로 실행되며, 더 이상 프로그램에서 사용하지 않는다고 판단되는 UObject들을 식별하여 메모리에서 제거함
      • 마크 앤 스윕 알고리즘 작동방식 3단계
        1. 루트셋에서 시작
          • 루트셋에 포함된 객체들을 식별 - 해당 객체들은 항상 살아있다고 간주되는 특별한 객체 ex)게임엔진, 플레이어 컨트롤러 등
        2. 마크 단계 - 도달 가능성 분석
          • 루트셋 객체에서 시작해서 직간접적으로 참조하는 UObject를 마크 - 객체가 사용중임을 나타냄
        3. 스윕 단계 - 메모리 회수
          • 마크 단계가 완료되면 마크되지 않은 객체들이 차지하고 있던 메모리를 회수 - 이 과정에서 해당 객체의 소멸자가 호출되고 메모리가 반환
    2. UObject
      • UObject에는 GC 동작 방식을 제어하는 다양한 플래그가 존재함
      • 해당 플래그들은 GC의 동작에 중요한 정보를 제공하며, GUObjectArray라는 전역 배열에 저장된 각 객체 정보의 일부로 관리됨.
      • RF_RootSet: 해당 플래그가 설정된 객체는 루트셋의 일부로 관리함, 즉 설정된 시점부터 가비지 컬렉션 대상이 아님. AddToRoot() 함수를 통해 설정하고, RemoveFromRoot()함수를 통해 해제할 수 있음
      • RF_BeginDestroyed: 객체의 BeginDestroy() 함수가 호출되었음을 나타냄. 객체가 실제로 메모리에서 해제되기 전에 필요한 정리 작업을 수행하는 함수.
      • RF_FinishedDestroyed: 객체의 FinishedDestroy() 함수가 호출되었음을 나타냄. 해당 함수는 객체 소멸의 마지막 단계로, 이 함수 호출 후 객체의 메모리가 완전히 배제됨
    3. 리플렉션 시스템
      • 리플렉션: 프로그램이 실행 중에 자신의 구조와 상태를 검사하고 수정할 수 있는 능력
      • C++의 경우 자체적인 리플렉션 기능이 없기 때문에 언리얼 엔진은 자체적인 리플렉션 시스템을 구축함
      • 리플렉션은 UObject를 위한 운영체제와 같기 때문에 언리얼 엔진 내부에서 동작하는 여러 모듈(GC, script system)은 모두 UObject 기반임.
      • 사용자가 정의한 타입들의 경우 엔진에서 알지 못하므로, 이를 처리할 수 있도록 타입 정보를 공유해야하는데 이를 위한 작업이 리플렉션이라고 함.
      • 핵심: UHT 코드 생성기
        • UHT는 C++ 컴파일러가 수행되기 전에 동작함. C++ 코드 내에서 메타 데이터를 얻고, 내부적으로 소스 코드를 생성함
        • 이 동작이 완료된 이후에 C++ 컴파일러가 수행됨.
        • 핵심 리플렉션 매크로
매크로 리플렉션에서의 목적 일반적인 위치
UCLASS() C++ 클래스를 UObject 기반의 리플렉션 시스템에 등록 클래스 정의 앞
UPROPERTY() 멤버 변수를 리플렉션 시스템에 노출 멤버 변수 선언 앞
UFUNCTION() 멤버 함수를 리플렉션 시스템에 노출 멤버 함수 선언 앞
USTRUCT() C++ 구조체를 리플렉션 시스템에 등록 구조체 정의 앞
GENERATED_BODY() UHT가 생성하는 리플렉션 및 엔진 지원 코드를 위한 삽입 지점 클래스/구조체 본문 첫 줄
  •  

리플렉션 기능을 종합한 예시 이미지

'C++' 카테고리의 다른 글

[C++]STL(Standard Template Library)  (1) 2025.06.11
[C++]템플릿  (0) 2025.06.09
[C++]프로그래밍 기초3  (0) 2025.06.02
[C++]프로그래밍 기초2  (0) 2025.05.29
[C++]프로그래밍 기초1  (0) 2025.05.28