I. C++의 메모리 관리
1) 메모리의 종류
프로그램이 사용하는 메모리는 크게 스택(Stack) 과 힙(Heap) 으로 나누어짐
| 구분 | 스택 (Stack) | 힙 (Heap) |
| 관리 | 컴파일러가 자동으로 관리해줌 | 프로그래머가 직접 관리해야 함 |
| 속도 | 빠름 | 느림 |
| 크기 | 작음 | 큼 |
| 생명주기 | 블록({}) 종료 시 자동 해제 | delete 할 때까지 유지 |
2) 스택 (Stack)
① 함수가 실행될 때 메모리의 모습
void Func()
{
int a = 10;
}
Stack
┌────────┐
│ a = 10 │
└────────┘
② 함수가 종료될 때(블록{}을 벗어날 때) 메모리의 모습
Stack
(비어 있음)
함수가 끝나는 순간 자동으로 메모리에서 사라짐
③ 특징
장점 - 빠름, 자동으로 관리됨, 메모리 누수가 없음
단점 - 크기가 작음, 함수가 끝나면 사라짐
3) 힙 (Heap)
직접 메모리를 빌리는 공간
Player* player = new Warrior(...);
Stack
player
│
▼
Heap
Warrior 객체
함수가 종료되어도 객체가 살아있음
delete player;
자동으로 없어지지 않고, 직접 delete를 해줘야 함
힙을 사용하는 이유
① 크기를 실행 중에 결정할 수 있음
cin >> size;
int* arr = new int[size];
사용자가 입력한 크기만큼 배열을 만들 수 있음
표준 C++에서는 int arr[size]; 불가능
② 함수가 끝나도 객체를 살아있게 할 수 있음
Player* CreatePlayer()
{
return new Warrior(...);
}
함수가 끝나도 Warrior 객체가 살아있음
③ 다형성
Player* player1 = new Warrior();
Player* player2 = new Magician();
Player* player3 = new Archer();
가상 함수를 이용해 override를 할 때 힙을 자주 이용함
4) 메모리 누수 (Memory Leak)
Player* player = new Warrior(...);
//delete player;
delete를 안 했기에 메모리에 객체가 계속 남아 있음
객체가 계속 쌓이면 메모리가 부족해짐
5) Dangling Pointer
메모리를 지웠는데, 포인터가 그대로 남아 있는 경우
Player* player = new Warrior();
delete player;
player->attack();
없어진 메모리를 가리키는 경우
delete player;
player = nullptr;
보통 이렇게 같이 씀
6) Double Delete
이미 지운 메모리를 또 지우는 경우
delete player;
delete player;
이 경우도 위험함
7) 스마트 포인터
힙을 쓸 때 직접 new와 delete를 쓰면 실수하기 쉬움
스마트 포인터는 자동으로 delete를 해줌
① unique_ptr
메모리를 소유하는 소유자가 딱 한 개
객체 하나를 관리할 때 자주 사용함
unique_ptr
│
▼
Warrior
unique_ptr<Player> p1 = make_unique<Warrior>();
unique_ptr<Player> p2 = p1; //복사 안 됨, 컴파일 에러 발생
move를 사용해서 소유권 이전만 가능, 이때 소유권이 없어진 쪽은 nullptr이 됨
unique_ptr<Player> p2 = move(p1);
p1
nullptr
p2
│
▼
Warrior
② shared_ptr
여러 포인터가 한 메모리를 같이 소유함
P1
\
\
Warrior
/
/
P2
shared_ptr<Player> p1 = make_shared<Warrior>();
cout << "p1의 레퍼런스 카운트: " << p1.use_count() << endl; //출력 1
shared_ptr<Player> p2 = p1;
cout << "p2 생성 후 레퍼런스 카운트: " << p1.use_count() << endl; //출력 2
p2.reset();
cout << "p2 해제 후 레퍼런스 카운트: " << p1.use_count() << endl; //출력 1
return 0;
}
내부적으로 Reference Count를 가짐
reset을 이용해 소유권을 해제할 수 있음
③ weak_ptr
서로를 shared_ptr로 참조하는 경우 레퍼런스 카운트가 0이 되지 않음, 이를 순환참조라고 함
A
↓
B
↑
A
이때, 한쪽을 weak_ptr으로 바꾸면 순환참조가 풀림
weak_ptr은 레퍼런스 카운트에 영향을 미치지 않아서, lock으로 유효성을 검사해야 함
#include <iostream>
#include <memory>
using namespace std;
class Monster {
public:
void roar() {
cout << "슬라임이 울부짖습니다!" << endl;
}
};
class Stage {
public:
weak_ptr<Monster> monster;
void checkMonster() {
if (auto currentMonster = monster.lock()) {
currentMonster->roar();
}
else {
cout << "몬스터가 이미 사라졌습니다." << endl;
}
}
};
int main() {
shared_ptr<Stage> stage = make_shared<Stage>();
{
shared_ptr<Monster> slime = make_shared<Monster>();
stage->monster = slime;
stage->checkMonster(); // 몬스터 존재
}
// slime 소멸
stage->checkMonster(); // 몬스터 없음
}
해설
shared_ptr<Monster> slime = make_shared<Monster>();
slime(shared_ptr)
│
▼
Monster
stage->monster = slime;
slime(shared_ptr)
│
▼
Monster
▲
│
monster(weak_ptr)
auto currentMonster = monster.lock();
slime(shared_ptr)
│
▼
Monster
▲
│
currentMonster(shared_ptr)
중괄호를 벗어나서 slime이 해제될 때, monster(weak_ptr)도 같이 해제됨
8) 얕은 복사와 깊은 복사
① 얕은 복사
포인터를 복사할 때, 값이 아니라 주소만 복사하는 것
int* a = new int(10);
int* b = a;
delete a;
cout << *b; // b -> dangling pointer
둘 다 같은 메모리를 가리킴
하나를 삭제하면, 나머지 하나는 dangling pointer가 됨
② 깊은 복사
포인터가 가리키는 메모리의 값을 새로운 메모리에 복제하는 것
int* a = new int(10);
int* b = new int(*a);
delete a;
cout << *b; //아무 문제 없음
II. 언리얼 엔진의 메모리 관리
1) 가비지 컬렉션 (Garbage Collection)
언리얼 엔진 내부에서는 new와 delete를 직접 쓰지 않음
대신에 Garbage Collection을 주로 사용함
작동 방식 - Mark & Sweep 알고리즘
주기적으로 실행됨
더 이상 프로그램에서 사용하지 않는다고 판단되는 객체를 메모리에서 제거함
① 루트셋 (Root Set)
루트셋에 포함된 객체를 식별
플레이 중에 항상 존재해야되는 객체를 포함함
② Mark 단계
루트셋의 객체에서 직간접적으로 참조할 수 있는 객체에 표시를 함
③ Sweep 단계
Mark 단계에서 표시되지 않은 객체를 메모리에서 제거함
2) 리플렉션 시스템
언리얼 엔진은 클래스 내부에 무엇이 들어있는 지 런타임 동안 모름
리플렉션 매크로를 사용하면 컴파일러의 수행 전에 사용자가 정의한 타입을 언리얼 엔진이 알게 해줌
자주 사용하는 매크로
| 매크로 | 역할 |
| UCLASS() | 클래스 등록 |
| USTRUCT() | 구조체 등록 |
| UPROPERTY() | 멤버 변수 등록 |
| UFUNCTION() | 함수 등록 |
| GENERATED_BODY() | 리플렉션 코드 삽입 |
예시
UCLASS(Blueprintable, BlueprintType)
class YOURPROJECT_API UPlayerData : public UObject
{
GENERATED_BODY()
public:
UPlayerData();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player")
int32 Level = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player")
float HP = 100.f;
UFUNCTION(BlueprintCallable, Category = "Player")
void LevelUp();
};'언리얼 엔진 > 개념 정리' 카테고리의 다른 글
| 개념 정리 - C++ (상속, 다형성) (0) | 2026.07.17 |
|---|---|
| 개념 정리 - C++(클래스) (0) | 2026.07.10 |
| 개념 정리 - C++(포인터와 레퍼런스) (0) | 2026.07.09 |
| 개념 정리 - C++(조건문과 반복문) (0) | 2026.07.08 |
| 개념 정리 - C++(배열과 함수) (0) | 2026.07.08 |