스택 메모리
- 변수의 생존 주기가 끝나면 선언 시 할당되었던 메모리가 자동으로 회수된다.
- 단점
- 일반적으로 할당 가능한 스택 메모리의 크기가 제한적
- 변수의 스코프를 벗어나면 자동 해제되므로 메모리를 더 길가나 유연하게 관리하기 힘듦
- 단점
힙 메모리
- 동적 할당 시 new 연산자를 사용하고, 해제 시 delete 연산자를 사용
- 스택과 달리 자동으로 해제되지 않으므로 메모리 누수 등의 위험의 있음
- 동적 할당된 객체 또는 변수의 생존 주기는 사용자가 delete로 해제할 때까지 유지
- new를 사용할때는 변수가 포인터형이 되어야한다.
- 사용 뒤에는 delete로 삭제해준다.
- 배열을 할당했다면 new int[5]처럼 선언하고, delete[]로 메모리를 해제한다.
#include <iostream>
using namespace std;
void func2() {
int* arr = new int[5];
for (int i = 0; i < 5; i++)
{
arr[i] = i * 10;
cout << "arr[" << i << "]=" << arr[i] << endl;
}
delete[] arr;
}
int main(){
func2();
return 0;
}
- 해제된 메모리의 주소를 계속 가지고 있는 포인터를 Dangling Pointer라고 하며, 이는 사용 후 반드시 삭제해주어야한다.
스마트 포인터
new/delete를 사용하지 않는 자동 메모리 관리
- unique_ptr
- 객체에 대한 단일 소유권 관리
- move를 통해 소유권을 이용하는 방식을 사용
- 단 하나의 포인터만 객체 소유 가능
- 소유권의 개념만 있기 때문에 복사 혹은 대입이 불가능
- shared_ptr
- 레퍼런스 카운트로 여러 개의 포인터가 한 객체를 참조할 수 있도록 관리
- 내부적으로 레퍼런스 카운터를 관리함
- use_count() 메서드를 활용하여 현재 객체를 참조하는 포인터의 수를 확인
- reset() 메서드로 소유 중인 객체를 해제하거나 다른 객체로 변경 가능
- weak_ptr
- 객체의소유권을 공유하지 않는 포인터
- 레퍼런스 카운트를 증가시키지 않는 약한 참조
- shared_ptr에서 발생하는 순환참조 문제를 해결함
- 순환참조란 두 개 이상의 객체가 서로를 shared_ptr로 가리켜 참조하는 상황
- 서로 순환하고 있는 shared_ptr 중 하나를 weak_ptr로 대체하면 순환 고리가 끊어짐
- shared_ptr은 관찰과 소유를 하는 반면, weak_ptr은 관찰만 한다.
- lock() 호출 후 반환된 shared_ptr이 유효한지 확인 후에 사용해야 함
기본적인 unique_ptr 사용법
#include <iostream>
#include <memory>
using namespace std;
int main(){
//unique_ptr 생성
unique_ptr<int> ptr1 = make_unique<int>(10);
//unique_ptr이 관리하는 값 출력
cout << "ptr의 값: " << *ptr1 << endl;
// unique_ptr은 복사가 불가능
// unique_ptr<int> ptr2 = ptr1; // 컴파일 에러 발생!
// 범위를 벗어나면 메모리 자동 해제
return 0;
}
move 사용
#include <iostream>
#include <memory>
using namespace std;
int main(){
//unique_ptr 생성
unique_ptr<int> ptr1 = make_unique<int>(20);
//소유권 이동(move)사용
unique_ptr<int> ptr2 = move(ptr1);
if (!ptr1)
{
cout << "ptr1은 이제 비어있습니다." << endl;
}
cout << "ptr2의 값: " << *ptr2 << endl;
// unique_ptr은 복사가 불가능
// unique_ptr<int> ptr2 = ptr1; // 컴파일 에러 발생!
// 범위를 벗어나면 메모리 자동 해제
return 0;
}
일반 클래스에서 unique_ptr 사용하는 방법
#include <iostream>
#include <memory>
using namespace std;
class MyClass {
public:
MyClass(int val) : value(val)
{
cout << "MyClass 생성: " << value << endl;
}
;
~MyClass() {
cout << "MyClass 소멸: " << value << endl;
}
void display() const {
cout << "값 : " << value << endl;
}
private:
int value;
};
int main(){
//unique_ptr 생성
unique_ptr<MyClass> myObject = make_unique<MyClass>(42);
// MyClass 멤버함수 호출
myObject->display();
//소유권 이동(move)사용
unique_ptr<MyClass> newOwner = move(myObject);
if (!myObject)
{
cout << "myObject는 이제 비어있습니다." << endl;
}
newOwner->display();
return 0;
}

기본적인 shared_ptr 사용법
#include <iostream>
#include <memory>
using namespace std;
class MyClass {
public:
MyClass(int val) : value(val)
{
cout << "MyClass 생성: " << value << endl;
}
;
~MyClass()
{
cout << "MyClass 소멸: " << value << endl;
}
void display() const {
cout << "값 : " << value << endl;
}
private:
int value;
};
int main(){
// shared_ptr 생성
shared_ptr<int> ptr1 = make_shared<int>(10);
// ptr1의 참조 카운트 출력
cout << "ptr1의 참조 카운트: " << ptr1.use_count() << endl; // 출력 1
// ptr2가 ptr1의 리소스를 공유
shared_ptr<int> ptr2 = ptr1;
cout << "ptr2 생성 후 참조 카운트: " << ptr1.use_count() << endl;
// ptr2가 범위를 벗어나면 참조 카운트 감소
ptr2.reset();
cout << "ptr2 해제 후 참조 카운트: " << ptr1.use_count() << endl;
// 범위를 벗어나면 ptr1도 자동 해제
return 0;
}

기본적인 weak_ptr 사용법
#include <iostream>
#include <memory>
using namespace std;
class A{
public:
void say_hello() {
cout << "Hello from A" << endl;
}
};
class B {
public:
weak_ptr<A> a_ptr;
void useA() {
if (auto a_shared = a_ptr.lock()) {
a_shared->say_hello();
}
else {
cout << "A is no longer available." << endl;
}
}
};
int main(){
// shared_ptr 생성
shared_ptr<B> b = make_shared<B>();
{
shared_ptr<A> a = make_shared<A>();
b->a_ptr = a;
b->useA(); // A가 유효하므로 Hello 출력
} // A는 scope를 벗어나며 소멸됨
b->useA(); // A는 이미 소멸되었기 때문에 메시지 출력
return 0;
}

shared_ptr을 사용해서 순환참조가 발생하는 예시
#include <iostream>
#include <memory>
using namespace std;
class B; // 먼저 선언하기
class A{
public:
shared_ptr<B> b_ptr;
~A() { cout << "A destroyed" << endl; }
};
class B {
public:
shared_ptr<A> a_ptr;
~B() {
cout << "B destroyed" << endl;
}
};
int main(){
// shared_ptr 생성
auto a = make_shared<A>();
auto b = make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// main 함수가 끝나도 A와 B는 서로 참조 중이라 메모리 해제가 안 됨
return 0;
}

순환참조 문제를 weak_ptr로 해결하기
#include <iostream>
#include <memory>
using namespace std;
class B; // 먼저 선언하기
class A{
public:
shared_ptr<B> b_ptr;
~A() { cout << "A destroyed" << endl; }
};
class B {
public:
weak_ptr<A> a_ptr; // weak_ptr로 변경
~B() {
cout << "B destroyed" << endl;
}
};
int main(){
// shared_ptr 생성
auto a = make_shared<A>();
auto b = make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // weak_ptr로 참조
return 0;
}

얕은 복사와 깊은 복사
- 얕은 복사
- 클래스 내의 포인터 멤버를 복사할 때 포인터가 가리키는 데이터가 아닌 포인터가 저장하고 있는 주소값만 복사
#include <iostream>
#include <memory>
using namespace std;
int main() {
// 포인터 A가 동적 메모리를 할당하고 값을 30으로 설정
int* A = new int(30);
// 포인터 B가 A가 가리키는 메모리를 공유
int* B = A;
cout << "A의 값: " << *A << endl; // 출력 : 30
cout << "B의 값: " << *B << endl; // 출력 : 30
// A가 동적 메모리를 해제
delete A;
// 이제 B는 Dangling Pointer(해제된 메모리를 가리키는 포인터)
// B에는 Undefined Behavior 발생
cout << "B의 값 (dangling): " << *B << endl;
return 0;
}

- 깊은 복사
- 클래스의 포인터 멤버가 기리키는 동적 데이터를 새로 할당된 독립적인 메모리 영역에 복제
#include <iostream>
#include <memory>
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의 값 (dangling): " << *B << endl;
return 0;
}

가비지 컬렉션
- 가비지 컬렉션은 더 이상 사용하지 않는 메모리를 자동으로 삭제해준다.
- C++에는 가비지 컬렉션이 없으므로 언리얼 엔진에서는 자체적으로 구현한 가비지 컬렉션 시스템을 사용
언리얼 엔진의 가비지 컬렉션
- 마크 앤 스윕 알고리즘 방식
- 필요없는 객체를 판별 후 삭제
- 루트셋에서 시작
- 루트셋에 포함된 객체들을 식별
- 이 객체들은 항상 살아있다고 간주되는 예외 객체로, 가비지 컬렉션 대상 아님
- 게임 엔진 자체, 플레이어 컨트롤러 등이 루트셋에 포함
- 마크 단계 - 도달 가능성 분석
- 루트셋 객체에서 시작해서 직간접적으로 참조하는 UObject를 마크
- 스윕 단계 - 메모리 회수
- 마크 단계가 완료되면 마크되지 않은 객체들이 차지한 메모리 회수
- 해당 객체의 소멸자를 호출하고 메모리 반환
- 더이상 사용되지 않는다고 판단한 UObject를 메모리에서 제거하는 방식
- UObject에는 가비지 컬렉션을 제어하는 플래그가 존재
- GUObjectArray라는 전역 배열에 저장된 각 객체 정보의 일부로 관리
- RF_RootSet
- 이 플래그가 설정된 객체는 루트셋의 일부로 관리
- AddToRoot() 함수를 통해 설정
- RemoveFromRoot() 함수를 통해 해제
- RF_BeginDestroyed
- 객체의 BeginDestroy() 함수가 호출되었음을 나타냄
- 객체가 실제로 메모리에서 해제되기 전에 필요한 정리 작업을 수행하는 함수
- 객체 메모리 회수하고 있음을 표시
- RF_FinishDestroyed
- FinishDestroy() 함수가 호출되었음을 나타냄
- 메모리 회수 후 호출되어 메모리 해제를 표시
언리얼 엔진의 리플렉션 시스템
- 리플렉션이란
- 프로그램이 실행 중에 자신의 구조와 상태를 검사하고 수정하는 능력
- C++의 경우 자체적인 리플렉션 기능이 없어 언리얼 엔진은 자체적인 리플렉션 시스템을 구축
- 리플렉션은 UObject를 위한 운영체제와도 같음
- 언리얼 엔진 내부에서 동작하는 여러 모듈(가비지 컬렉터, 스크립트 시스템) 등은 모두 UObject 기반
- 그러나 사용자가 정의한 타입들의 경우 엔진에서 알지 못함
- 이를 처리할 수 있도록 타입 정보를 공유해야하며, 이 작업이 리플렉션
- UHT(Unreal Engine Tool 코드 생성기
- 코드를 컴파일해서 실행할 때, 언리얼 엔진 모듈에서 사용자 정의 객체 등을 이해할 수 있도록 정보를 주는 툴
- 사용자 객체가 언리얼 모듈과 동일하게 동작하게 하여 자원 관리가 용이하게 하고 기존에 제공했던 기능을 활용할 수 있게 함
- UHT는 C++ 컴파일러가 수행되기 전에 동작하여 메타데이터를 추가
- 언리얼 엔진의 핵심 리플렉션 매크로
- UCLASS()
- C++ 클래스를 UObject 기반의 리플렉션 시스템에 등록
- 클래스 정의 앞
- UPROPERTY()
- 멤버 변수를 리플렉션 시스템에 노출
- 멤버 변수 선언 앞
- UFUNCTION()
- 멤버 함수를 리플렉션 시스템에 노출
- USTRUCT()
- C++ 구조체를 리플렉션 시스템에 등록
- 구조체 정의 앞
- GENERATED_BODY()
- UHT가 생성하는 리플렉션 및 엔진 지원 코드를 위한 삽입 지점
- 클래스/구조체 본문 첫 줄에 반드시 포함해줘야함
- UCLASS()
'코딩 학습 > C와 C++' 카테고리의 다른 글
| C++ 기초 오답노트 / 기능 정리 (0) | 2026.03.12 |
|---|---|
| C++ 기초 - 템플릿 (0) | 2026.03.11 |
| C++ 기초 - Vector, Map (0) | 2026.03.09 |
| C++ 기초 - 클래스 (0) | 2026.03.06 |
| C++ 기초 - 포인터와 레퍼런스 (0) | 2026.03.05 |