코딩 학습/C와 C++

C++ 기초 - 자원 관리

이개 2026. 3. 10. 21:01

스택 메모리

  • 변수의 생존 주기가 끝나면 선언 시 할당되었던 메모리가 자동으로 회수된다.
    • 단점
      • 일반적으로 할당 가능한 스택 메모리의 크기가 제한적
      • 변수의 스코프를 벗어나면 자동 해제되므로 메모리를 더 길가나 유연하게 관리하기 힘듦

힙 메모리

  • 동적 할당 시 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를 사용하지 않는 자동 메모리 관리

  1. unique_ptr
    1. 객체에 대한 단일 소유권 관리
    2. move를 통해 소유권을 이용하는 방식을 사용
    3. 단 하나의 포인터만 객체 소유 가능
    4. 소유권의 개념만 있기 때문에 복사 혹은 대입이 불가능
  2. shared_ptr
    1. 레퍼런스 카운트로 여러 개의 포인터가 한 객체를 참조할 수 있도록 관리
    2. 내부적으로 레퍼런스 카운터를 관리함
    3. use_count() 메서드를 활용하여 현재 객체를 참조하는 포인터의 수를 확인
    4. reset() 메서드로 소유 중인 객체를 해제하거나 다른 객체로 변경 가능
  3. weak_ptr
    1. 객체의소유권을 공유하지 않는 포인터
    2. 레퍼런스 카운트를 증가시키지 않는 약한 참조
    3. shared_ptr에서 발생하는 순환참조 문제를 해결함
      1. 순환참조란 두 개 이상의 객체가 서로를 shared_ptr로 가리켜 참조하는 상황
      2. 서로 순환하고 있는 shared_ptr 중 하나를 weak_ptr로 대체하면 순환 고리가 끊어짐
    4. shared_ptr은 관찰과 소유를 하는 반면, weak_ptr은 관찰만 한다.
    5. 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가 생성하는 리플렉션 및 엔진 지원 코드를 위한 삽입 지점
      • 클래스/구조체 본문 첫 줄에 반드시 포함해줘야함

'코딩 학습 > 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