[내일배움캠프/C++] 메모리 관리

김세희·2025년 5월 30일

✍️Today I Learned

  1. char to int
  2. C++의 메모리 관리
  3. 언리얼엔진의 메모리 관리

char
문자 하나를 저장하는 데이터 타입이지만 내부적으로는 아스키코드에 해당하는 정수값으로 저장된다.
따라서 char를 int로 변환하면 아스키코드 값을 반환한다.

char to int의 간단한 방법

char c = '5';
int i = c - '0';

C++의 메모리 관리

메모리는 한정된 자원이다.

🎯스택 메모리

  • 스택은 컴파일 시간에 크기가 결정된다.
  • 일반 변수들은 대부분 스택 메모리 공간을 차지한다.
  • 변수의 생존 주기({ })가 끝나면 선언 시 할당되었던 메모리가 자동으로 회수된다.
  • 생존 주기는 {}내부이기 때문에 함수에서도 반환값이 없으면 함수가 끝날 때 로컬 변수의 메모리가 회수된다. 그래서 포인터나 레퍼런스를 사용해 외부 변수의 값을 조작한다.

단점

  • 일반적으로 할당 가능한 메모리 크기가 제한적이다.
  • 변수 스코프를 벗어나면 자동으로 해제되어 메모리를 길거나 유연하게 관리하기 어렵다.

🎯힙(동적) 메모리

  • ❗동적으로 메모리를 할당한다 = 코드 실행 시점에 메모리를 할당한다. 사용자에게 입력을 받아 할당할 수 있다.
  • 메모리 전체 크기: 힙 > 스택
  • 동적 할당 시 new 연산자를 사용하고 해제 시 delete연산자를 사용한다.
  • 자동 해제되지 않으므로 메모리 누수 등의 위험이 있다.
  • 사용자가 delete로 해제할 때까지 유지된다.
  • new는 주소를 반환하기 때문에 lvalue가 포인터여야한다.

배열의 동적 할당과 해제

new <type>[number]: 힙 메모리를 배열처럼 사용할 수 있다. 이렇게 할당한 메모리는 delete[]으로 해제해야 한다.

int *p1 = new int;
delete p1;

int *p2 = new int[100];
delete[] p2;
// delete p2; // error

Dangling Pointer

❗이미 해제된 메모리의 주소를 계속 가지고 있는 포인터
포인터는 가리키는 메모리가 해지되었는지 알 수 없음
해당 포인터의 주소를 읽기만 할때는 정상 종료는 될 수 있으나 제어하면 에러가 발생한다.
이중 해지도 위험!

메모리 누수

동적 할당한 메모리를 사용 후 해제하지 않으면 사용하지 않는 메모리가 쌓이고 결국 메모리가 부족해질 수 있다.

🎯스마트 포인터

Dangling Pointer가 발생하지 않도록 관리해주는 역할
new, delete를 사용하지 않는 자동 메모리 관리이다.
스마트 포인터는 객체에 대해 소유권을 갖는다.
메모리 해제 책임을 명확하게 하고, 자동으로 처리하기 위해서
스마트 포인터는 포인터처럼 작동하는 클래스 객체이다.

unique_ptr

단일 소유권을 관리한다.
여러 포인터가 동시에 하나의 주소를 소유할 수 없다. move로 소유권을 이전할 수 있다.

shared_ptr

여러 포인터가 하나의 주소를 소유할 수 있다.
레퍼런스 카운터로 몇개의 포인터가 소유하고 있는지 관리한다.
레퍼런스 카운트가 0이되면 메모리가 해지된다.
use_count(): 현재 객체를 참조하는 포인터의 수 출력
reset(): 소유 중인 객체를 해제하거나 다른 객체로 변경
순환 참조
두 개 이상의 객체가 서로 shared_ptr를 가리켜 참조하는 상황으로 메모리 누수를 유발한다.

weak_ptr

다른 스마트 포인터와 다르게 레퍼런스 카운트를 증가시키지 않는 약한 참조를 한다.\
순환 참조일 때 서로 순환하고 있는 shared_ptr 중 하나를 weak_ptr로 대체하면 순환 고리가 끊어져 문제를 해결할 수 있다.
lock(): 내부 객체 유효성 확인 메소드. 약한 참조는 유효성 확인하고 사용해야한다.
weak_ptr는 자기 자신이 소유할 수 없고 shared_ptr를 통해서 간접적으로 관찰하기 때문에
'lock()'은 유효할 경우 shared_ptr<객체>를 반환한다. 아니면 nullptr에 해당하는 shared_ptr를 반환한다.

#include <iostream>
#include <memory>

class A {
public:
    void say_hello() {
        std::cout << "Hello from A\n";
    }
};

class B {
public:
    std::weak_ptr<A> a_ptr;

    void useA() {
    	//a_shared에 a_ptr.lock()의 반환값을 넣고 a_shared가 nullptr인지 확인하는 부분
        if (auto a_shared = a_ptr.lock()) { // 유효한지 확인 
            a_shared->say_hello();
        } else {
            std::cout << "A is no longer available.\n";
        }
    }
};

int main() {
    std::shared_ptr<B> b = std::make_shared<B>();
    
    {
        std::shared_ptr<A> a = std::make_shared<A>();
        b->a_ptr = a;
        b->useA(); // A가 유효하므로 Hello 출력
    } // A는 scope을 벗어나며 소멸됨

    b->useA(); // A는 이미 소멸되었기 때문에 메시지 출력
}

얕은 복사와 깊은 복사

얕은 복사: 클래스 내의 포인터 멤버를 복사할 때 포인터의 주소만 복사하는 것. dangling pointer가 발생할 수 있다.
깊은 복사: 클래스의 포인터 멤버가 가리키는 동적 데이터를 새로 할당된 독립된 메모리에 복제하는 것. 포인터나 동적으로 할당된 자원을 관리하는 객체는 메모리 안정성을 위해 깊은 복사를 사용하는 것이 좋다.

#include <iostream>
using namespace std;

int main() {
    // 포인터 A가 동적 메모리를 할당하고 값을 30으로 설정
    int* A = new int(30);

    // 포인터 B가 A가 가리키는 값을 복사 (깊은 복사)
    int* B = new int(*A);

    cout << "A의 값: " << *A << endl; // 출력: 30
    cout << "B의 값: " << *B << endl; // 출력: 30

    // A가 동적 메모리를 해제
    delete A;

    // B는 여전히 독립적으로 자신의 메모리를 관리
    cout << "B의 값 (깊은 복사 후): " << *B << endl; // 출력: 30

    // B의 메모리도 해제
    delete B;

    return 0;
}

언리얼엔진의 메모리 관리

가비지 컬렉션 시스템으로 객체들의 메모리 관리를 자동화한다.

🎯가비지 컬렉션

더이상 사용하지 않는 객체들의 메모리를 알아서 해제한다.

  • Mark & Sweep
    : 주기적으로 실행된다. 더이상 사용하지 않는다고 판단되는 UObject를 식별하여(Maek) 메모리에서 제거(Sweep)한다. 루트셋(가비지 컬렉션 대상이 아님)에서 시작해서 사용 중인 UObject를 마크하고 완료되면 마크되지 않은 객체들의 메모리를 회수한다. 이 과정에서 해당 객체의 소멸자가 호출되고 메모리가 반환된다.
  • 플래그
    : 가비지 컬렉션 동작을 제어. GUObjcectArray라는 전역 배열을 통해 게임의 오브젝트를 관리하는데 거기에서 플래그를 통해 객체 정보의 일부로 관리된다.
    1. RF_RootSet
      : 루트셋의 일부로 설정. 가비지 컬렉션 대상이 아니다. AddToRoot(),RemoveFromRoot()
    2. RF_BeginDestroyed (사용자가 설정하는거 아님)
      : 메모리 회수 단계에서 BeginDestroy()함수가 호출된 상태를 나타낸다. 객체가 메모리에서 해제되기 전에 필요한 정리 작업을 수해하는 함수.
    3. RF_FinishDestroyed (사용자가 설정하는거 아님)
      : FinishDetroy()함수가 호출되었음을 나타낸다. 객체 소멸의 마지막 단계로 이후 객체의 메모리가 완전히 해제된다.

🎯언리얼엔진의 리플렉션 시스템

리플렉션 시스템

  • 프로그램이 실행 중에 자신의 구조와 상태를 검사하고 수정할 수 있는 능력
  • UObject를 위한 운영체제
  • 사용자가 정의한 객체를 언리얼엔진 모듈이 이해하도록 설명하는 과정(UHT)
  • 리플렉션 매크로를 추가해 UHT 코드 생성기로 진행한다.
  • 사용자 객체가 언리얼 모듈과 동일하게 동작하게 하며 자원 관리가 용이하게 하고 기존에 제공했던 기능을 활용할 수 있게 한다.
리플렉션 매크로목적일반적인 위치
UCLASS()C++ 클래스를 UObject 기반의 리플렉션 시스템에 등록클래스 정의 앞
UPROPERTY()멤버 변수를 리플렉션 시스템에 노출멤버 변수 선언 앞
UFUNCTION()멤버 함수를 리플렉션 시스템에 노출멤버 함수 선언 앞
USTRUCT()C++ 구조체를 리플렉션 시스템에 등록구조체 정의 앞
GENERATED_BODY()UHT가 생성하는 리플렉션 및 엔진 지원 코드를 위한 삽입 지점클래스/구조체 본문 첫 줄

0개의 댓글