TIL(19)

이채원·2026년 7월 8일

TIL

목록 보기
19/77

오늘의 할일
1. 코드카타 정리
2. 세션 정리
3. 과제
4. 궁금증 해결
5. 다음 학습 예습

개념 요약

멤버 함수 : 필요한 동작 정의. 외부에서도 접근 가능
멤버 변수 : 세부 데이터 관리(class 자체에서 내부 연산할 때 필요한 정보). 외부에서 접근하지 못하게 함
멤버함수 구현 (클래스 내부, 외부)

  • 외부에서 구현할 때는 범위 지정 연산자 :: 사용

클래스의 멤버 함수나 멤버 변수에 접근할 때는

  1. 객체 뒤에 멤버 접근 연산자 . 을 사용

  2. 접근 지정자(public , private , protected)를 사용하여 멤버의 접근 권한을 제어
    -> 접근 지정자를 명시하지 않으면 기본적으로 private로 설정

public 아래에 적힌 변수나 함수는 프로그램 어디에서나, 누구나 자유롭게 접근해서 보고 쓸 수 있음
ex) 캐릭터의 이름, 레벨, 혹은 '공격하기', '방어하기' 같은 행동(함수)

private 아래에 적힌 것들은 오직 그 클래스 내부에 있는 함수들만 접근할 수 있음. 외부에서는 절대 볼 수도, 수정할 수도 없음
ex) 캐릭터의 실제 체력(hp), 마나(mp), 방어력 수치

protected : private이랑 거의 똑같이 외부의 접근을 완벽하게 차단. but 나를 상속받은 자식 클래스만은 접근을 허락

private 에 있는 변수를 제어해야 할 때
-> 멤버 변수를 바꿀 때 setter를, 값을 가져올 때 getter를 사용

생성자 : 객체를 생성할 때마다 한 번씩 자동으로 호출되는 특별한 멤버 함수
-> 필요한 멤버 변수를 초기화하거나 객체가 동작할 준비를 하기 위해 사용
-> 반환형을 명시하지 않고, class 이름과 동일한 이름을 가진 함수로 정의

생성자 호출 순서

  1. 인스턴스화 -> 정의된 class(붕어빵 틀)를 변수로 선언하면 해당 객체(붕어빵)가 메모리에 올라가게 하기
  2. 객체가 생성될 때, 멤버 변수를 포함해서 필요한 정보들이 메모리에 올라가게 하기 -> 생성자 호출됨

다형성 : 기본이 되는 클래스를 만들어 함수의 인터페이스를 정의하고 실제 구현은 파생 클래스에서 담당하게 하는 기법
동적 바인딩 : 게임 실행 중에(동적으로) 짝을 지어준다(바인딩)

  • 동적 바인딩을 통해 실제 호출된 객체의 타입에 따라 적절한 파생 클래스의 함
    수가 실행

virtual int func() -> 가상 함수 테이블 생성
--> 가상 함수 테이블에는 기본 클래스에서 virtual로 선언된 함수가 파생 클래스에서 재정의된 경우 그 위치가 기록되어 동적 디스패치가 가능

  • 만약 virtual 을 붙이지 않는다면, 파생 클래스에서 func 메서드를 호출할 경우 Base의 func 메서드가 호출. 실제 호출한 파생 클래스의 함수 정보를 알 수 없기 때문
  • virtual을 붙이는 경우, 가상 함수 테이블을 참조하여 실제 호출한 파생 클래스의 메서드가 호출

순수 가상 함수 : 기본 클래스에서 구현하지 않고, 반드시 파생 클래스에서 구현하도록 강제하는 가상 함수(인터페이스는 필요하지만 기본 클래스에서 구현할 필요가 없는 경우)
-> 가상 함수를 정의하고 " = 0" 을 붙여주면 됨

virtual int memFunc() = 0;

추상 클래스 : 순수 가상 함수를 하나 이상 포함하는 클래스
-> 직접 인스턴스를 생성할 수 없고, 포인터 혹은 참조를 통해서만 사용

스택 메모리 -> 일반 변수들은 대부분 스택 메모리 공간을 차지
-> 변수의 생존 주기가 끝나면 선언 시 할당되었던 메모리가 자동으로 회수

단점 :
1. 일반적으로 할당 가능한 스택 메모리의 크기가 제한적
2. 변수의 스코프(생존 영역)을 벗어나면 자동으로 해제되므로, 메모리를 더
길거나 유연하게 관리하기 어렵

힙 메모리 크기 > 스택 메모리 크기

힙 메모리

  • 동적 할당 시 new 연산자를 사용/ 해제 시 delete 연산자를 사용
  • 스택과 달리 자동으로 해제되지 않으므로 메모리 누수 등의 위험
  • 동적 할당된 객체(또는 변수)의 생존 주기는 사용자가 delete로 해제할 때까지 유지
    --> new, delete는 주소를 반환하기 때문에 무조건 포인터로 받아야함

배열의 동적 할당과 해제

  • new 타입[] / delete[] 기억하기

Dangling Pointer : 이미 해제된 메모리의 주소를 계속 가지고 있는 포인터

스마트 포인터 -> Dangling Pointer가 발생하지 않도록 자동으로 관리(new / delete 를 사용하지 않음)

  • unique_ptr : 객체에 대한 단일 소유권 관리
    -> move를 통해 소유권을 이동하는 식으로 관리
    -> 단 하나의 포인터만 객체를 소유할 수 있음
  • shared_ptr : 레퍼런스 카운트(현재 객체를 참조하는 포인터의 개수를 카운팅) 관리
    -> 여러 개의 포인터가 하나의 객체를 공유 가능
    -> 레퍼런스 카운트가 0이 되면 객체는 자동으로 메모리 해제
    -> shared_ptr은 관찰과 소유

    use_count() 메서드를 활용하여 현재 객체를 참조하는 포인터의 수를 확인
    reset() 메서드로 소유 중인 객체를 해제하거나 다른 객체로 변경

  • weak_ptr : 객체의 소유권을 공유 X
    -> 레퍼런스 카운트를 증가시키지 않는 약한 참조
    -> weak_ptr은 관찰만

    shared_ptr는 순환참조가 발생 가능 -> 메모리 누수를 유발
    순환 참조 : 두 개 이상의 객체가 서로를 shared_ptr로 가리켜 참조하는 상황
    해결 -> 서로 순환하고 있는 shared_ptr 중 하나를 weak_ptr로 대체

  • lock() 호출 후 반환된 shared_ptr 이 유효한지 확인 후에 사용

얕은 복사 : 클래스 내의 포인터 멤버를 복사할 때 포인터가 가리키는 데이터가 아닌 포인터가 저장하고 있는 주소값만 복사하는 것
-> dangling pointer가 발생 가능

깊은 복사 : 클래스의 포인터 멤버가 가리키는 동적 데이터를 새로 할당된 독립적인 메모리 영역에 복제하는 것
-> 원본 객체, 복사된 객체는 서로 독립적인 메모리 공간 소유 -> dangling pointer가 발생 X

☑ 가비지 컬렉션은 마크 앤 스윕 알고리즘 방식으로 동작

  1. 루트셋에서 시작
  • 먼저 루트셋에 포함된 객체들(살아있다고 간주)을 식별
    ex) 게임 엔진 자체, 플레이어 컨트롤러 등
    이는 가비지 컬렉션 대상이 아님 XXX
  1. 마크 단계 - 도달 가능성 분석
  • 루트셋 객체에서 시작해서 직간접적으로 참조하는 UObject를 마크. (객체가 사용중)
  1. 스윕 단계 - 메모리 회수
  • 마크 단계가 완료되면 마크되지 않은 객체들이 차지하고 있던 메모리를 회수. 이 과정에서 해당 객체의 소멸자가 호출되고 메모리가 반환됩니다.

☑ UObject에는 가비지 컬렉션 동작 방식을 제어하는 다양한 플래그가 존재

  • 이 플래그들은 가비지 컬렉션의 동작에 중요한 정보를 제공, GUObjectArray라는 전역 배열에 저장된 각 객체 정보의 일부로 관리

📌 RF_RootSet

이 플래그가 설정된 객체는 루트셋의 일부로 관리 -> 즉 설정된 시점부터 가비지 컬렉션 대상이 아님 X
AddToRoot() 함수를 통해 설정, RemoveFromRoot() 함수를 통해 해제

📌 RF_BeginDestroyed

객체의 BeginDestroy() 함수가 호출되었음을 나타냄
BeginDestroy() : 객체가 실제로 메모리에서 해제되기 전에 필요한 정리 작
업을 수행하는 함수

📌 RF_FinishDestroyed

객체의 FinishDestoy() 함수가 호출되었음을 나타냄
FinishDestoy() : 객체 소멸의 마지막 단계. 함수 호출 후 객체의 메모리가 완전히 해제

리플렉션 시스템 : 프로그램이 실행 중에 자신의 구조와 상태를 검사하고 수정할 수 있는 능력
-> 사용자가 정의한 타입들의 경우 엔진에서 알지 함, 이를 처리할 수 있도록 타입 정보를 공유해야 함


코드카타 정리

자연수 N이 주어지면, N의 각 자릿수의 합을 구해서 return 하는 solution 함수

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>

int solution(int n) {
    int answer = 0;
    while (n > 0)
    {
        answer += n%10;
        n = n/10;
    }
    return answer;
}

게임 개발자를 위한 C++ 문법 : 3회차 정리

# Class

실제 현업에서 프로젝트를 진행 -> 완성된 프로젝트의 유지 보수를 용이하게 하고, 재사용성을 높이는 방향
그래서 사용되는 게 객체지향 프로그래밍 !

객체 없이 성적 관리 프로그램 만들기
-> 문제점 : 과목의 개수가 늘어나면 함수의 인자가 추가되고 성적 관리를 하는 데이터가 모두 노출되면서 비효율적인 코드가 완성

  • 멤버 함수 : 필요한 동작 정의. 외부에서도 접근 가능하게 함
  • 멤버 변수 : 세부 데이터 관리(class 자체에서 내부 연산할 때 필요한 정보). 외부에서 접근하지 못하게 함

Student Class 구현

class 의 멤버 함수를 구현하는 방법
1. 클래스 내부에서 멤버 함수의 본문까지 직접 정의하는 방법
2. 클래스 내부에서 멤버 함수의 선언만 작성하고, 클래스 외부에서 구현하는 방법

멤버함수 구현 (클래스 내부, 외부)

  • 외부에서 구현할 때는 범위 지정 연산자 :: 사용

## 접근제어(클래스의 멤버에)

클래스의 멤버 함수나 멤버 변수에 접근할 때는

  1. 객체 뒤에 멤버 접근 연산자 . 을 사용

  2. 접근 지정자(public , private , protected)를 사용하여 멤버의 접근 권한을 제어
    -> 접근 지정자를 명시하지 않으면 기본적으로 private로 설정

  • private 멤버 : 클래스 외부에서 직접 접근할 경우 컴파일 에러가 발생
  • public 멤버 : 클래스 외부에서 멤버 접근 연산자로 접근 가능

public 아래에 적힌 변수나 함수는 프로그램 어디에서나, 누구나 자유롭게 접근해서 보고 쓸 수 있음
ex) 캐릭터의 이름, 레벨, 혹은 '공격하기', '방어하기' 같은 행동(함수)

private 아래에 적힌 것들은 오직 그 클래스 내부에 있는 함수들만 접근할 수 있음. 외부에서는 절대 볼 수도, 수정할 수도 없음
ex) 캐릭터의 실제 체력(hp), 마나(mp), 방어력 수치

protected : private이랑 거의 똑같이 외부의 접근을 완벽하게 차단. but 나를 상속받은 자식 클래스만은 접근을 허락

class Player {
private:
    // [비밀 금고] 외부에서 절대 함부로 바꿀 수 없음!
    int hp = 100; 

protected:
    // [가문 창고] 외부 접근 불가, 하지만 나중에 직업(전사, 마법사)을 만들면 걔네는 볼 수 있음
    int baseStat = 10; 

public:
    // [마을 광장] 외부에서 자유롭게 사용할 수 있는 기능들
    string name;

    void takeDamage(int damage) {
        // 내부 함수(자기 자신)니까 private인 hp를 마음대로 건드릴 수 있음!
        hp -= damage; 
        if (hp < 0) hp = 0;
    }
};

int main() {
    Player p1;
    p1.name = "John";      // OK! (public이니까 가능)
    p1.takeDamage(20);     // OK! (public 함수를 통해서 간접적으로 hp 감소)
    
    // p1.hp = -999;       // 에러! ❌ (private이라서 밖에서 직접 건드릴 수 없음!)
    // p1.baseStat = 50;   // 에러! ❌ (protected라서 밖에서 못 건드림!)
}

## getter 와 setter

private 에 있는 변수를 제어해야 할 때
-> 멤버 변수를 바꿀 때 setter를, 값을 가져올 때 getter를 사용

  • setter

    • setMathScore(int math)
    • setEngScore(int eng)
    • setKorScore(int kor)
  • getter

    • getMathScore()
    • getEngScore()
    • getKorScore()

## 생성자

생성자 : 객체를 생성할 때마다 한 번씩 자동으로 호출되는 특별한 멤버 함수
-> 필요한 멤버 변수를 초기화하거나 객체가 동작할 준비를 하기 위해 사용
-> 반환형을 명시하지 않고, class 이름과 동일한 이름을 가진 함수로 정의

생성자 호출 순서

  1. 인스턴스화 -> 정의된 class(붕어빵 틀)를 변수로 선언하면 해당 객체(붕어빵)가 메모리에 올라가게 하기
  2. 객체가 생성될 때, 멤버 변수를 포함해서 필요한 정보들이 메모리에 올라가게 하기 -> 생성자 호출됨
class Player {
public:
    int hp; // 1. 설계도에 "방 1개 필요함" 이라고 적어둠

    // 이것이 바로 생성자! (클래스 이름과 똑같은 특수 함수)
    Player() { 
        hp = 100; // 입주 청소 완료!
        cout << "플레이어가 생성되었습니다!" << endl;
    }
};

int main() {
    // 이 한 줄이 실행될 때 컴퓨터 안에서 일어나는 일:
    // (1) Player 크기만큼의 메모리(빈 방)를 먼저 확보함!
    // (2) 그 직후에 Player() 생성자가 쏙 들어가서 hp를 100으로 세팅함!
    Player p1; 
    
    return 0;
}

기본 생성자(인자가 없는)

매개변수가 있는 생성자

기본매개변수가 있는 생성자

잘못된 매개변수 전달로 에러 발생

선언만 하고 정의하지 않아 에러 발생

기본 생성자를 잘못 호출해서 에러 발생

## student class 구현해보기

기본값이 적용된 생성자

헤더 파일(선언부)에 class 를 정의하는 것 -> 목차 만들기
소스 파일(구현부)에 세부 구현하는 것 -> 실제 책 내용

  1. student.h에 Class 정의

  2. student.cpp에 class 구현

  3. 메인 함수에서 정의한 class 사용(정의된 class가 포함된 Student.h를 include)

## 숙제 : 배터리 관리 클래스

📌 기능
배터리 잔량(0~100)을 관리합니다.
배터리 사용 및 충전을 할 수 있어야 합니다.
초기 배터리는 0입니다.
📌 구현할 메서드
배터리 잔량을 반환하는 메서드
배터리를 사용하는 메서드 (한번 호출될 때마다 배터리 잔량은 5씩 감소)
배터리를 충전하는 메서드 (한번 호출될 때마다 배터리 잔량은 7씩 충전)
📌 초기 상태
배터리 잔량의 기본값은 100입니다.
해당 값은 생성자에서 설정할 수 있어야 합니다.

헤더파일

소스파일

메인함수

## 숙제2 : 두 개의 분수 곱셉 클래스

📌 Fraction 클래스를 만들어 분수를 표현
분자는 numerator , 분모는 denominator 로 관리
두 분수의 곱의 결과는 기약분수로 출력
📌 구현할 메서드
기본 생성자 생성. (numerator = 0 / denominator = 1 )
분자의 분모의 수를 입력받는 생성자를 구현
현재 분수를 출력하는 함수 display 를 구현
기약분수 형태로 만드는 함수 simplify 를 구현
이때 최대공약수를 구하는 함수가 필요할 수 있는데 아래를 참조

// gcd 함수 구현 (유클리드 호제법)
int gcd(int a, int b) {
 while (b != 0) {
 int temp = b;
 b = a % b;
 a = temp;
 }
 return a;
}

헤더파일

소스파일

메인함수

# 객체지향 프로그래밍

## 상속

자동차의 속도 같은 공통된 특성을 하나의 클래스로 묶어놓고 각각의 차이점(바퀴 갯수)을 구현하면 효율적

기본 클래스 : 상속 대상이 되는 클래스
파생 클래스 : 기본 클래스를 상속받아 새로운 클래스를 정의

부모 클래스 Vehicle 클래스 구현

Bycycle 클래스와 Truck 클래스를 구현(ex : class Bicycle : public Vehicle)

  • Bicylce 클래스와 Truck 클래스의 생성자는 속도와 색상 값 초기화 시
    부모클래스인 Vehicle 클래스의 생성자를 사용

Vehicle 클래스

Biycle / Truck 클래스

메인함수

## 다형성

동물 울음소리 내기(다형성 적용 X)

  • 모든 동물 클래스가 유사한 개요 가짐

다형성 : 기본이 되는 클래스를 만들어 함수의 인터페이스를 정의하고 실제 구현은 파생 클래스에서 담당하게 하는 기법
동적 바인딩 : 게임 실행 중에(동적으로) 짝을 지어준다(바인딩)

  • 동적 바인딩을 통해 실제 호출된 객체의 타입에 따라 적절한 파생 클래스의 함
    수가 실행

virtual int func() -> 가상 함수 테이블 생성
--> 가상 함수 테이블에는 기본 클래스에서 virtual로 선언된 함수가 파생 클래스에서 재정의된 경우 그 위치가 기록되어 동적 디스패치가 가능

  • 만약 virtual 을 붙이지 않는다면, 파생 클래스에서 func 메서드를 호출할 경우 Base의 func 메서드가 호출. 실제 호출한 파생 클래스의 함수 정보를 알 수 없기 때문
  • virtual을 붙이는 경우, 가상 함수 테이블을 참조하여 실제 호출한 파생 클래스의 메서드가 호출

다형성이 적용된 코드(bark는 가상 함수로 정의)

  • Animal이라는 인터페이스를 만들고 Animal 포인터 호출

몬스터 공격 다형성 넣어서 구현 그림

[1단계: 포인터]                 [2단계: 실제 객체(알맹이)]
Monster* mob ─────────────────> ┌───────────────────────────┐
(겉포장지: 몬스터)              │ Slime 객체 메모리        │
                              │                         │
                              │ ┌──────────────────────┐  │      [3단계: 비밀 지도]
                              │ │ vptr (가상 함수 포인터) │─ ─ ─> ┌──────────────────────────┐
                              │ └──────────────────────┘  │       │ Slime 가상 함수 테이블   │
                              │                         │       │ (VTable)               │
                              │ - hp: 100               │       │                        │
                              │ - mp: 50                │       │ ┌──────────────────────┐ │            [4단계: 실제 실행]
                              └────────────────────────────┘      │ │ 1. attack() 주소    │─────── ─ ─> ┌───────────────────┐
                                                                │ ├──────────────────────┤ │          │ Slime::attack()  │
                                                                │ │ 2. defend() 주소    │ │          │ 끈적하게 공격!    │
                                                                │ └──────────────────────┘ │          └───────────────────┘
                                                                └──────────────────────────┘

가상 함수 예시

파생 클래스의 공통된 인터페이스를 제공하여 다형성을 구현하기 위해서 무조건 기본 클래스의 virtual을 구현하는 게 아니어도 됨

👇👇👇👇👇👇👇

순수 가상 함수 : 기본 클래스에서 구현하지 않고, 반드시 파생 클래스에서 구현하도록 강제하는 가상 함수(인터페이스는 필요하지만 기본 클래스에서 구현할 필요가 없는 경우)
-> 가상 함수를 정의하고 " = 0" 을 붙여주면 됨

virtual int memFunc() = 0;

추상 클래스 : 순수 가상 함수를 하나 이상 포함하는 클래스
-> 직접 인스턴스를 생성할 수 없고, 포인터 혹은 참조를 통해서만 사용

## 숙제 : 다형성을 활용한 게임 스킬 사용 프로그램

📌 기본 클래스
Adventure 라는 기본 클래스를 정의하세요.
useSkill() 이라는 순수가상함수를 선언하세요.
📌 파생 클래스
Warror , Mage , Archer 라는 세 가지 파생 클래스를 만드세요. 각 클래스의 useSkill 함수를 재정의 해서 아래와 같이 출력하세요.
Warror : Warror uses Slash!
Mage : Mage casts Fireball!
Archer : Archer shoots an Arrow!
📌 다형성 구현
Adventure* 타입의 포인터를 사용하여 여러 모험가 객체를 가리키고, 반복문을 통해 각 모험가의 스킬을 호출하는 동작을 구현하세요.

다양한 직업을 가진 모험가들이 각기 다른 스킬을 사용하는 프로그램을 구현

# 자원 관리하기(메모리 관리)

메모리는 한정된 자원!

## 스택 메모리

일반 변수들은 대부분 스택 메모리 공간을 차지
-> 변수의 생존 주기가 끝나면 선언 시 할당되었던 메모리가 자동으로 회수

단점 :
1. 일반적으로 할당 가능한 스택 메모리의 크기가 제한적
2. 변수의 스코프(생존 영역)을 벗어나면 자동으로 해제되므로, 메모리를 더
길거나 유연하게 관리하기 어렵

## 힙 메모리

힙 메모리 크기 > 스택 메모리 크기

  • 동적 할당 시 new 연산자를 사용/ 해제 시 delete 연산자를 사용
  • 스택과 달리 자동으로 해제되지 않으므로 메모리 누수 등의 위험
  • 동적 할당된 객체(또는 변수)의 생존 주기는 사용자가 delete로 해제할 때까지 유지

--> new, delete는 주소를 반환하기 때문에 무조건 포인터로 받아야함

변수 하나의 영역에 대한 동적할당

배열의 동적 할당과 해제

  • new 타입[] / delete[] 기억하기

메모리 해지를 하지 않는 예시
-> delete가 없기 때문에 함수 호출때마다 힙 메모리가 쌓임(메모리 누수)

사용자 입력 기반으로 배열 동적 할당

  • 코드 실행 -> size -> 힙 메모리 할당 (스택은 실행 전에 크기 할당)

## Dangling Pointer

Dangling Pointer : 이미 해제된 메모리의 주소를 계속 가지고 있는 포인터

Dangling Pointer 예시

  • 메모리가 해지된 이후는 이상한 값이 들어있음

메모리를 이중 해지 하는 예시
-> 주석 풀면 두번째 사진처럼 예외 발생

## 스마트 포인터

스마트 포인터 -> Dangling Pointer가 발생하지 않도록 자동으로 관리(new / delete 를 사용하지 않음)

  • unique_ptr : 객체에 대한 단일 소유권 관리
    -> move를 통해 소유권을 이동하는 식으로 관리
    -> 단 하나의 포인터만 객체를 소유할 수 있음
    • 소유권의 개념만 있기 때문에 복사 혹은 대입이 불가능 -> 시도하는 순간 컴파일 에러가 발생
      • 기본적인 uinque_ptr 사용법

        move로 unique_ptr 소유권 이전

        일반 클래스에서 unique_ptr 사용하는 방법
  • shared_ptr : 레퍼런스 카운트(현재 객체를 참조하는 포인터의 개수를 카운팅) 관리
    -> 여러 개의 포인터가 하나의 객체를 공유 가능

    기본적인 shared_ptr 사용법

    일반 클래스에서 shared_ptr 사용법

-> 레퍼런스 카운트가 0이 되면 객체는 자동으로 메모리 해제
-> shared_ptr은 관찰과 소유

use_count() 메서드를 활용하여 현재 객체를 참조하는 포인터의 수를 확인
reset() 메서드로 소유 중인 객체를 해제하거나 다른 객체로 변경

  • weak_ptr : 객체의 소유권을 공유 X
    -> 레퍼런스 카운트를 증가시키지 않는 약한 참조
    -> weak_ptr은 관찰만

    shared_ptr는 순환참조가 발생 가능 -> 메모리 누수를 유발
    순환 참조 : 두 개 이상의 객체가 서로를 shared_ptr로 가리켜 참조하는 상황
    해결 -> 서로 순환하고 있는 shared_ptr 중 하나를 weak_ptr로 대체

  • lock() 호출 후 반환된 shared_ptr 이 유효한지 확인 후에 사용

    lock()함수로 유효성을 확인하고 weak_ptr를 사용하는 예시

## 얕은 복사와 깊은 복사

얕은 복사 : 클래스 내의 포인터 멤버를 복사할 때 포인터가 가리키는 데이터가 아닌 포인터가 저장하고 있는 주소값만 복사하는 것
-> dangling pointer가 발생 가능

깊은 복사 : 클래스의 포인터 멤버가 가리키는 동적 데이터를 새로 할당된 독립적인 메모리 영역에 복제하는 것
-> 원본 객체, 복사된 객체는 서로 독립적인 메모리 공간 소유 -> dangling pointer가 발생 X

## 가비지 컬렉션 시스템

언리얼 엔진은 객체들의 메모리 관리를 자동화하기 위해 가비지 컬렉션 시스템을 사용

☑ 가비지 컬렉션은 마크 앤 스윕 알고리즘 방식으로 동작
-> 주기적으로 실행되며, 더 이상 프로그램에서 사용하지 않는다고 판단되는 UObject 들을 식별하여 메모리에서 제거

  1. 루트셋에서 시작
  • 먼저 루트셋에 포함된 객체들(살아있다고 간주)을 식별
    ex) 게임 엔진 자체, 플레이어 컨트롤러 등
    이는 가비지 컬렉션 대상이 아님 XXX
  1. 마크 단계 - 도달 가능성 분석
  • 루트셋 객체에서 시작해서 직간접적으로 참조하는 UObject를 마크. (객체가 사용중)
  1. 스윕 단계 - 메모리 회수
  • 마크 단계가 완료되면 마크되지 않은 객체들이 차지하고 있던 메모리를 회수. 이 과정에서 해당 객체의 소멸자가 호출되고 메모리가 반환됩니다.
[주최자(Root)] ──(직접 초대)──> [Object 1] 🟢 생존!
     │
     └───(직접 초대)──> [Object 4] ──(건너건너 초대)──> [Object 3] 🟢 둘 다 생존!
                            ▲
                            │ (나도 너 아는데 껴주면 안 돼?)
                        [Object 2] 🔴 주최자 쪽에서 오는 길이 없어서 폐기 처분! (GC 대상)
                        
                        

☑ UObject에는 가비지 컬렉션 동작 방식을 제어하는 다양한 플래그가 존재

  • 이 플래그들은 가비지 컬렉션의 동작에 중요한 정보를 제공, GUObjectArray라는 전역 배열에 저장된 각 객체 정보의 일부로 관리

📌 RF_RootSet

이 플래그가 설정된 객체는 루트셋의 일부로 관리 -> 즉 설정된 시점부터 가비지 컬렉션 대상이 아님 X
AddToRoot() 함수를 통해 설정, RemoveFromRoot() 함수를 통해 해제

📌 RF_BeginDestroyed

객체의 BeginDestroy() 함수가 호출되었음을 나타냄
BeginDestroy() : 객체가 실제로 메모리에서 해제되기 전에 필요한 정리 작
업을 수행하는 함수

📌 RF_FinishDestroyed

객체의 FinishDestoy() 함수가 호출되었음을 나타냄
FinishDestoy() : 객체 소멸의 마지막 단계. 함수 호출 후 객체의 메모리가 완전히 해제

예시

호텔 체크아웃으로 보는 GC 플래그
RF_RootSet (VIP 등록): "이 손님은 VIP니까 청소부(GC)는 절대 쫓아내지 마!"

RF_BeginDestroyed (짐 싸기): "손님, 이제 체크아웃 시간입니다. 나가기 전에 짐 챙기세요." (내부 데이터 정리)

RF_FinishDestroyed (방 비우기): "체크아웃 완료. 이제 완전히 빈 방입니다." (메모리 완전 삭제)

// 1. 객체 생성 (호텔 방에 손님 입장)
UObject* MyItem = NewObject<UObject>();

// 📌 [RF_RootSet 켜짐] : "이 손님 VIP 등록이요! 청소부님 건드리지 마세요!"
MyItem->AddToRoot(); 

// ... 게임 플레이 중 ...

// 📌 [RF_RootSet 꺼짐] : "VIP 기간 끝났습니다. 이제 청소부가 치워도 됩니다."
MyItem->RemoveFromRoot(); 


// ====================================================
// 아래부터는 우리가 아니라 '언리얼 엔진 청소부(GC)'가 알아서 하는 일
// ====================================================

// 📌 [RF_BeginDestroyed 켜짐] : 청소부가 노크함 (자동 실행됨)
// MyItem->BeginDestroy(); 
// 엔진 曰: "너 곧 삭제될 거니까 화면에서 사라질 준비 하고 짐 싸!"

// 📌 [RF_FinishDestroyed 켜짐] : 청소부가 방을 비움 (자동 실행됨)
// MyItem->FinishDestroy();
// 엔진 曰: "짐 다 쌌지? 이제 진짜 방 뺀다! (메모리 완전 소멸)"

## 리플렉션 시스템

리플렉션 시스템 : 프로그램이 실행 중에 자신의 구조와 상태를 검사하고 수정할 수 있는 능력
-> 사용자가 정의한 타입들의 경우 엔진에서 알지 함, 이를 처리할 수 있도록 타입 정보를 공유해야 함 -> 리플렉션 시스템!

❌ [리플렉션 안 된 일반 C++ 객체] 👻
"나 체력 100짜리 몬스터야!" ──(무시)──> 언리얼 엔진: "누구세요? 사원증 없으면 못 들어옵니다."


✅ [리플렉션 시스템 통과 (UCLASS, UPROPERTY 추가)] 🪪✨
"여기 제 상세 프로필(타입 정보)이 적힌 사원증입니다!" 
                 │
                 ▼
      [ 🏢 언리얼 엔진의 핵심 부서들 ]
                 │
                 ├──> 🗑️ [가비지 컬렉터 부서]: "아하! 이 객체는 우리가 관리(청소)해주면 되겠군!"
                 │
                 ├──> 🎨 [에디터 디스플레이 부서]: "오! 디테일 패널 화면에 '체력 100'이라고 띄워줄게!"
                 │
                 └──> 🧩 [블루프린트 부서]: "좋아! 블루프린트 노드로 드래그해서 체력을 깎을 수 있게 해줄게!"
                 

과제


궁금증 해결하기

1

Q. 클래스 내부에서 멤버 함수 정의 VS 클래스 외부에서 멤버 함수 정의

  1. 클래스 내부에서 정의하기 (인라인 방식) - 보통 한 두줄 분량의 함수
    -> 클래스 중괄호 { } 안에 함수의 내용(구현부)을 전부 다 적어버리는 방식

장점 (가볍고 빠름): 한곳에 모여 있어서 간단한 함수를 빠르게 작성할 때 아주 편함.

C++ 컴파일러가 이 함수를 inline 함수로 자동 처리해 줄 확률이 높아져. (함수를 호출하는 시간을 생략하고 코드를 직접 끼워 넣어줘서, 아주 미세하게 실행 속도가 빨라질 수 있어.)

단점 (지저분함): 함수 내용이 수십 줄씩 길어지면, 정작 이 클래스(설계도)에 어떤 기능들이 있는지 한눈에 파악하기가 너무 힘들어져.

  1. 클래스 외부에서 정의하기 - 보통 복잡하고 긴 함수
    -> 클래스 안에는 이름(선언)만 남겨두고, 실제 내용은 클래스 바깥으로 빼서 적는 방식

장점 (깔끔한 설계도와 분업화): 클래스 내부가 깔끔해져서, 어떤 기능이 있는지 한눈에 알아보기 좋음.

나중에 코드가 길어지면 선언부(class)는 헤더 파일(.h)에, 구현부(진짜 내용)는 소스 파일(.cpp)에 쪼개서 저장하게 되는데, 이때 무조건 외부 정의 방식을 써야 함.

단점 (귀찮음): 함수를 만들 때마다 앞에 Player:: (범위 지정 연산자)를 계속 붙여줘야 해서 타자 칠 게 늘어남.
.
.
.

2

Q. Inline에 대하여

컴퓨터가 일반 함수를 실행할 때는 "하던 일을 멈춤 -> 함수의 주소로 점프 -> 함수 실행 -> 다시 원래 위치로 돌아옴"이라는 복잡한 과정을 거침. 이 왔다 갔다 하는 시간을 오버헤드(Overhead)라고 함.

함수 앞에 inline을 붙여주면, 컴파일러가 코드를 번역할 때 함수를 호출하는 대신, 그 함수의 코드를 호출한 자리에 통째로 '복사+붙여넣기' 함.

inline void addHP(int& hp) {
    hp += 10;
}

int main() {
    int myHP = 100;
    addHP(myHP); // 컴퓨터는 이걸 볼 때 함수로 안 가고...
    // myHP += 10; <- 이렇게 코드를 복붙해서 통째로 바꿔치기해버림!
}

장점 (스피드): 함수를 왔다 갔다 하는 시간이 사라져서 프로그램 실행 속도가 미세하게 빨라짐.

단점 (프로그램 뚱뚱해짐): 만약 100줄짜리 긴 함수를 인라인으로 만들고 50군데에서 사용하면 컴퓨터가 100줄짜리 코드를 50군데에 전부 복사+붙여넣기 해버림. 그럼 최종 완성된 프로그램 파일(exe)의 용량이 엄청 커져버림. (-> 코드 블로트, Code Bloat)


예습

profile
초보 개발자

3개의 댓글

comment-user-thumbnail
2026년 7월 14일

예습....크흠

2개의 답글