[UE5 TIL] Day 9 - C 복습/과제 작성

JungHoon Eum·2026년 3월 9일

오늘 배운 내용 요약

  • 스택 메모리와 힙 메모리의 차이를 다시 정리
  • new, delete, delete[]의 역할과 동적 할당의 기본 구조를 복습했다.
  • Dangling Pointer와 Memory Leak이 왜 위험한지 확인했다.
  • 스마트 포인터 unique_ptr, shared_ptr, weak_ptr의 차이를 배웠다.
  • weak_ptr는 반드시 lock()으로 확인 후 사용해야 함을 이해했다.
  • 참조 카운트가 언제 증가하고 감소하는지 추적해 보았다.
  • ~MyClass(){} 형태의 소멸자가 객체 소멸 시 실행된다는 점을 정리했다.
  • Logger 과제를 통해 std::string, unique_ptr, -> 문법을 같이 복습했다.
  • unique_ptr의 단일 소유권과 싱글턴 보장은 완전히 같은 말이 아니라는 점이 헷갈렸고, 이를 따로 정리했다.

키워드

  • 스택 메모리
  • 힙 메모리
  • 동적 할당
  • Dangling Pointer
  • 스마트 포인터
  • unique_ptr
  • shared_ptr
  • weak_ptr
  • lock()
  • Reference Count
  • 소멸자
  • 얕은 복사 / 깊은 복사
  • 가비지 컬렉션
  • UObject
  • 루트셋
  • 리플렉션

C++ 녹화강의

자원 관리

  • 메모리는 한정된 자원. 넣은 걸 빼야 관리가 된다.

스택 메모리

  • 일반 변수
  • 변수 생존 주기가 끝나면(스코프) 메모리 자동 회수
    • 가장 가까운 }가 생존 주기

힙 메모리

  • 스택 메모리의 단점:
    • 할당 가능한 메모리의 크기가 제한적
    • 변수의 스코프를 벗어나면 자동 해제. 메모리를 길거나 유연하게 관리 어려움
  • 동적 할당: int* ptr = new int(10) / delete ptr
    • new 는 무조건 포인터로 받아야 한다 why? new 의 반환값은 '주소' 이기 때문.
  • 해제 제대로 안하면 메모리 누수의 위험 존재
  • 배열 대상으로 할 땐 new int[5], delete[] arr 식으로 배열인걸 표기해 줘야 함
    • 전 시간에 한 것처럼, 이렇게 만든 배열은 포인터처럼 사용 가능. 고대로 arr[i] 사용.
  • 동적 할당을 사용자 입력 받아서 사용도 가능
    cin >> size;  // 배열 크기를 사용자로부터 입력받음

    if (size > 0) {
        int* arr = new int[size];  
        // 입력받은 크기만큼 동적 배열 생성
        for (int i = 0; i < size; ++i) {
            arr[i] = i * 2;  // 배열 초기화

Dangling Pointer

  • 포인터는 메모리 해제 여부를 자동으로 알 수 없음
    -이미 해제된 메모리 주소를 계속 가지고 있는 포인터를 Dangling Pointer 라고 부른다
    • 기본적으로 피해야 할 대상
	int* ptr = new int(40);  // 힙 메모리에 정수 40 할당
	int* ptr2 = ptr;
......
 	delete ptr; //ptr2 는 어캄??????
  • 읽기만 해서 뭐가 문제가 크게 생기진 않는다. 종료는 된다. 값만 이상해짐
    • 쓰기 시작하면 개지랄 시작
  • 이중 해지(delete 2번) -> 프로그램 실행이 안됨

Memory Leak

  • 사용한 메모리는 항상 해제하기

스마트 포인터

  • 메모리 관리를 어느 정도 관리해 주는, dangling pointer 발생을 방지해 주는 포인터
  • newdelete를 사용하지 않는 자동 메모리 관리
  • unique_ptr: 객체에 대한 단일 소유권 관리. 소유권 '이전' 은 가능한데 두 포인터가 한 객체를 소유는 불가능
  • shared_ptr: 여러 포인터가 한 객체 소유 가능.
    • 이걸 Reference Count로 관리 -> 사용자가 수정 불가능한 값
    • Reference Count 가 0이 되면 메모리 해제
  • weak_ptr: "소유권" 개념이 없는 약한 참조
    • Reference Count를 증가시키지 않음
    • 두 포인터가 shared_ptr 이면 순환참조(서로가 서로를 가르킴) 가능성이 있어 메모리 누수 발생 가능
    • 하나를 weak_ptr로 하면 문제 X
  • shared_ptr은 관찰/소유, weak_ptr 은 관찰만

Unique Pointer

  • 소유권의 개념만 있다. = move(ptr); 로 이동
  • 복사 혹은 대입 불가능 -> 컴파일 에러 발생
    • ptr2 = ptr1 이 안되는거지 *ptr2 = *ptr1이 안되는건 아니다.
  unique_ptr<int> ptr1 = make_unique<int>(20);

    // 소유권 이동 (move 사용)
    unique_ptr<int> ptr2 = move(ptr1);

Shared Pointer

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; // 출력: 2

    // ptr2가 범위를 벗어나면 참조 카운트 감소
    ptr2.reset();
    cout << "ptr2 해제 후 참조 카운트: " << 
    ptr1.use_count() << endl; // 출력: 1
  • ptr.use_count(), ptr.reset() 기억하기
  • ~MyClass(){}: 클래스 소멸 시 실행

weak_ptr

-반드시 lock()으로 반환된 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() {
        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는 이미 소멸되었기 때문에 메시지 출력
}
  • A 클래스는 걍 함수 1개짜리 검증용 클래스.
  • B 클래스는 멤버변수로 weak pointer a_ptr 가지고 있음. A를 가리킬 (수도 있음)
    • lock() 을 안하면 유효할지 안할지 모름. 걍 선언은 누구나 할 수 있다 느낌?
  • B는 멤버 함수로 useA() 를 가지고 있음.
    • a_ptr.lock()std::weak_ptr(위에 위크포인터 선언할 때 썼던, <memory> 라이브러리에 속한 std) 속한 멤버함수.
      weak_ptr.lock()으로 쓰면 해당 weak pointer가 가리키는 주소에 대한 shared pointer를 반환함
      -auto는 그냥 자동으로 자료형 정해달라는것. 정확히는 shared_ptr a_shared 라는 변수를 if문 안에 생성해서 얘가 "존재하냐 안하냐" 로 if문을 돌리는 거
      • a_shared 는 그냥 if문 돌리는 동안만 살아있는 수명이 존나짧은 shared pointer다. 이 동안에만 reference count는 2.
  • main 함수에선
    • 먼저 B 클래스에 대한 shared pointer 생성.
    • 그리고 중괄호 생성
      • 여기서 A 클래스에 대한 shared pointer a 생성.
      • b 포인터로 만들어진 B의 인스턴스의 a_ptra 대입
      • 스마트 포인터라 포인터 생성->메모리 할당->객체 생성 같이 이루어짐에 주목
    • 이후 중괄호 닫히고 shared pointer a 는 수명이 다한다. reference count 0이라 객체 자체도 당연히 사망
      • why? 중괄호 밖에 남아 있는 B클래스의 a_ptr 는 weak pointer 니까
    • 이후 b의 멤버함수 useA() 사용하려 해봤자 A는 없다.

얕은 복사, 깊은 복사

  • 포인터/동적으로 할당된 자원을 관리하는 객체는 깊은 복사
  • 얕은 복사: 포인터가 가리키는 데이터가 아닌 포인터가 가진 "주소" 만 복사. 즉 같은 동적 메모리 영역을 가리킨다.
    • 원본 객체가 메모리를 해제하면, 복사된 객체 포인터는 해제된 메모리 영역 가리킴. Dangling Pointer!
  • 말은 어렵지만, 그냥 쉽게
    int* B = A; 하지 말고
    int* B = new int(*A); 하라는 거임

언리얼 엔진의 메모리 관리

  • 가비지 컬렉션 시스템: 알아서 엔진에서 메모리 치워주는 시스템 있음.
  1. 루트셋 객체 확인. 얘네는 삭제 안함.
  2. 루트셋에서 직간접적으로 참조하는 Object들 마크 -> 얘넨 사용중.
  3. 마크되지 않은 애들 메모리 회수. 객체들 소멸자 호출. 메모리 반환.
  • UObject: 오브젝트들. GUObjectArray = 전역에 있는 오브젝트들의 정보가 담긴 배열. 플래그로 지워도 되는지 필요없는지 관리
  • AddToRoot(), RemoveFromRoot() 으로 루트셋 관리. RF_RootSet 플래그 있으면 절대 삭제안함
  • RF_BeginDestroyed, RF_FinishDestoryed 플래그로 얘 지워라~얘 진짜 소멸시킴 단계

리플렉션

  • 사용자가 정의한 C++ 클래스, 타입들을 UObject 기반의 시스템으로 활용가능하게 해주는.

강의 숙제

  • 로그 메시지는 중요도에 따라(Info, Warning, Error)
    로 분류되어 기록될 수 있습니다. 로그 앞에 중요도가 태그가 붙어 표시됩니다.
    [Info] 사용자 메시지
    [Warning] 사용자 메시지
    [Error] 사용자 메시지
  • 로그 기록기는 단 하나의 인스턴스만 존재해야 하며,
    unique_ptr를 통해 이를 보장해야 합니다.
  • 지금까지 기록된 로그의 총개수를 출력할 수 있어야 합니다.
#include <iostream>
#include <memory>
#include <string>

class Logger {
private:
	int logCount=0;

public:
	Logger(){
		std::cout << "Logger Instance has been created" << std::endl;
	}
	~Logger(){
		std::cout << "Logger Instance terminated. Total log count: " << logCount<< std::endl;
	}
	void logInfo(std::string message){
		std::cout << "[INFO] " << message << std::endl;
		logCount++;
	}
	void logWarning(std::string message){
		std::cout << "[WARNING] " << message << std::endl;
		logCount++;
	}
	void logError(std::string message){
		std::cout << "[ERROR] " << message << std::endl;
		logCount++;
	}
	void showTotalLogs(){
		std::cout << "Total log count is: " << logCount << std::endl;
	}
};



int main(void){
	std::unique_ptr<Logger> LogInstance = std::make_unique<Logger>();
	LogInstance->logInfo("System is starting.");
	LogInstance->logWarning("Low disk space.");
	LogInstance->logError("Unable to connect to the server.");
	LogInstance->showTotalLogs();
	
	return 0;
}

과제 포인트

  • String 자료형을 받기 위해:
    • #include <string>
    • 함수 인풋에 std::string message
    • main()에선 그냥 "" 로 받으면 된다
  • unique_ptrLogInstance 가 하나만 존재하게 보장하진 않지않나...?
profile
개발지망생

0개의 댓글