[내일배움캠프] 본 과정 학습내용

[내배캠] 10일차 C++ 학습 내용 정리

홍이니 2026. 7. 9. 20:23

안녕하세요!

어제는 스택 메모리와 힙 메모리까지 진행하고 과제를 하였는데요.

오늘은 어제 내용에 이어서 계속 진행해 보도록 하겠습니다.

 

댕글링 포인터(Dangling Pointer)

이미 해제된 메모리의 주소 값을 가지고 있는 포인터를 댕글링 포인터(Dangling Pointer)라고 합니다.

두 포인터가 같은 메모리의 주소 값을 가지고 있을 때, 한 포인터가 delete 키워드로 메모리를 해제하면 같은 주소 값을 가지고 있던 또 하나의 포인터가 댕글링 포인터(Dangling Pointer)가 되는 것입니다.

 

댕글링 포인터(Dangling Pointer)가 위험한 이유는 해제된 메모리에 접근 하는 것은 예상할 수 없는 동작을 일으킬 수 있고 Segmentation fault(세그먼테이션 오류) 즉, 접근이 허용되지 않은 메모리 영역에 접근했을 때 나타나는 오류가 발생합니다.

추가로 댕글링 포인터(Dangling Pointer)는 메모리 주소와 관련된 문제이기 때문에 잠재적으로 보안에 위험이 될 수 있습니다.

따라서 댕글링 포인터(Dangling Pointer)가 생기지 않도록 조심해야 합니다.

 

**특히 댕글링 포인터(Dangling Pointer)가 더 위험한 것은 값을 읽어올 때(Read) 보다 값을 쓸 때(Write) 입니다.**

 

메모리 누수(Memory Leak)

이전 글에서도 학습했듯이 동적으로 할당된 메모리를 사용 후에 해제하지 않으면 그 공간을 계속 차지하게 됩니다.

물론 하나라도 없어야 되겠지만 한 영역이나 두 영역 정도 일 때는 프로그램과 컴퓨터에 크게 문제되지 않을 수 있겠지만 이런 메모리 누수가 많아져 메모리를 사용할 수 없는 상태가 되고 프로그램과 컴퓨터에 문제가 생길 수 있기 때문에 매우 조심해야 합니다.

 

스마트 포인터(Smart Pointer)

동적으로 메모리를 할당하고 사용하는 것은 매우 유용하고 다양한 장점을 제공하지만, 메모리를 직접 관리해야하기 때문에 위험하고 부담스러울 수 있습니다.

그래서 C++에서는 Dangling Pointer가 생기지 않도록 자동으로 관리해주는 스마트 포인터(Smart Pointer)를 제공합니다.

스마트 포인터(Smart Pointer)는 new 키워드와 delete 키워드를 사용하지 않고 메모리를 동적 할당할 수 있도록 기능합니다.

 

스마트 포인터는 #include <memory> 라이브러리를 통해서 사용할 수 있으며, 스마트 포인터의 종류는 총 3가지로 구성되어 있습니다.

 

// 스마트 포인터 (Smart Pointer)

#include <iostream>
#include <string>
#include <memory>

using namespace std;

int main(void)
{
    // 유니크 포인터
    printf("// unique_ptr\n");
    {
        unique_ptr<int> x = make_unique<int>(10);
        printf("unique_ptr<int> x = make_unique<int>(10); => %d\n", *x);

        unique_ptr<int> y = move(x);
        printf("unique_ptr<int> y = move(x); => %d\n\n", *y);
    }

    // 쉐어드 포인터
    printf("// shared_ptr\n");
    {
        shared_ptr<int> x = make_shared<int>(20);
        printf("make shared_ptr x\nx.use_count(); => %d\n", x.use_count());

        shared_ptr<int> y = x;
        printf("make shared_ptr y\ny.use_count(); => %d\n", y.use_count());

        shared_ptr<int> z = y;
        printf("make shared_ptr z\nz.use_count(); => %d\n\n", z.use_count());

        x.reset();
        printf("x.reset();\n");
        printf("y.use_count(); => %d\n\n", y.use_count());
    }

    // 위크 포인터
    printf("// weak_ptr\n");
    {
        weak_ptr<int> w;
        printf("make weak_ptr w\n\n");

        shared_ptr<int> x = make_shared<int>(30);
        printf("make shared_ptr x\nx.use_count(); => %d\n", x.use_count());

        shared_ptr<int> y = x;
        printf("make shared_ptr y\ny.use_count(); => %d\n", y.use_count());

        shared_ptr<int> z = y;
        printf("make shared_ptr z\nz.use_count(); => %d\n\n", z.use_count());

        x = z;
        printf("x = z;\n");
        printf("y.use_count(); => %d\n\n", y.use_count());

        z = w.lock();
        printf("z = w.lock();\n");
        printf("y.use_count(); => %d\n\n", y.use_count());

        x.reset();
        printf("x.reset();\n");
        printf("y.use_count(); => %d\n\n", y.use_count());
    }

    return 0;
}

 

unique_ptr(유니크 포인터)

단일 소유권을 관리하는 스마트 포인터 입니다. 즉, 한 메모리 공간이 다수의 포인터을 가지지 않도록 관리하여 객체의 소유권을 명확히 합니다.

unique_ptr(유니크 포인터)의 경우, 소유권 이동이 가능하여 효율적인 자원 관리가 가능합니다.

기본 형태는 unique_ptr<자료형> 포인터명 = make_unique<자료형>(주소 안의 값); 의 형태를 갖춥니다.

 

소유권의 이동은 move 키워드를 통해 할 수 있습니다.

이동 형태는 nuique_ptr<자료형> 포인터명 = move(값을 넘겨주고자 하는 유니크 포인터명); 의 형태를 갖춥니다.

 

shared_ptr(쉐어드 포인터)

한 주소 값이 복수의 포인터를 가질 수 있는 스마트 포인터로 shared_ptr(쉐어드 포인터)는 내부적으로 레퍼런스 카운트를 가지고 주소 값에 연결된 포인터를 관리합니다.

레퍼런스 카운트는 주소 값에 연결된 포인터의 수 만큼 레퍼런스 카운트의 값을 가지고 연결된 포인터가 해제될 때마다 레퍼런스 카운트를 감소하고 레퍼런스 카운트가 0이 되면 할당된 메모리를 해제합니다.

기본 형태는 shared_ptr<자료형> 포인터명 = make_shared<자료형>(주소 안의 값); 의 형태를 갖춥니다.

 

포인터명.use_count() 함수를 통해 레퍼런스 카운트의 값을 확인할 수 있습니다.

포인터명.reset() 함수를 통해 소유 중인 객체를 연결 해제하거나 다른 객체로 변경할 수 있습니다.

 

weak_ptr(위크 포인터)

소유권을 가지지 않는 스마트 포인터 즉, 약한 참조를 하는 스마트 포인터 입니다.

weak_ptr(위크 포인터)가 필요한 이유는 shared_ptr(쉐어드 포인터)의 경우, 순환 참조가 발생할 수 있기 때문입니다.

"순환 참조"란 두 개 이상의 객체가 서로를 shared_ptr(쉐어드 포인터)로 가리키는 경우에 메모리 누수가 발생할 수 있습니다.

누수가 발생하는 이유는 서로가 서로를 가리키고 있어 레퍼런스 카운트가 0이 되지 않기 때문입니다.

이럴 때, 한 포인터를 weak_ptr(위크 포인터)로 변환하여 종단점을 만들어주면 순환을 탈출할 수 있게 됩니다.

weak_ptr(위크 포인터)는 소유권을 가지지 않습니다.

 

포인터명.lock() 함수를 통해 shared_ptr(쉐어드 포인터)가 유효한지 확인한 후 사용해야만 합니다.

 

얕은 복사와 깊은 복사

포인터를 복사할 때 값은 그대로 가지고 오면 얕은 복사, 독립적인 메모리 공간을 할당하고 그 곳에 값을 옮겨서 가지고 오면 깊은 복사가 됩니다.

 

얕은 복사는 댕글링 포인터를 만들 위험이 있습니다.

댕글링 포인터를 만들 위험이 있는 이유는 얕은 복사는 값을 그대로 가지고 옵니다. 즉, 포인터 변수나 동적으로 할당된 자원의 경우에 주소 값이 같은 메모리 공간을 바라보게되고 어느 곳에선가 그 메모리 공간을 해제 해버리면 남은 객체 혹은 변수는 댕글링 포인터를 가지게 됩니다. 따라서 포인터나 동적으로 할당된 자원을 관리할 때에는 깊은 복사를 사용하는 것이 좋습니다.

 

깊은 복사는 복사 원본이 할당을 해제해도 복사본이 댕글링 포인터가 되지않고 복사 원본의 값을 가지고 있을 수 있도록 복사는 것을 의미합니다.

 

Unreal에서의 메모리 관리

가비지 컬렉터가 더 이상 사용되지 않는 객체를 식별하여 가비지 컬렉션안에 넣고 확인이 끝나면 가비지 컬렉션 안의 객체의 메모리 공간을 해제합니다. 가비지 컬렉터가 식별하기 위해서는 Unreal에서 제공하는 UObject 객체 타입으로 생성해야 합니다.

가비지 컬렉터는 마크 앤 스윕 알고리즘 방식으로 작동합니다. 마크 앤 스윕 알고리즘은 (1) 먼저 절대 해제 되면 안되는 객체들을 확인하고 넘어갑니다. 절대 해제되면 안되는 객체들은 루트셋(RootSet)안에 들어가 있습니다. (2) 다음으로 현재 사용 중이라고 식별되는 객체에 마크(mark), 표시합니다. (3) 표시되지 않은 객체들의 메모리 공간을 해제합니다. 이 과정에서 해당 객체의 소멸자가 호출됩니다.

 

UObject에는 가비지 컬렉터의 동작 방식을 제어하는 함수가 있습니다.

어떤 객체를 루트셋 즉, 절대 해제되지 않는 객체로 설정하고 싶을 때는 AddToRoot() 함수를 통해 루트셋에 넣을 수 있고 루트셋에서 삭제할 때는 RemoveFromRoot() 함수를 통해 루트셋에서 뺄 수 있습니다.

가비지 컬렉터가 표시하지 않은 객체들 즉, 메모리 공간이 해제될 객체들은 BeginDestroy() 함수를 통해 메모리에서 해제되기 전에 필요한 정리 작업을 수행하고 FinishDestroy() 함수를 호출해 메모리 공간을 해제합니다.

 

Unreal의 리플렉션

Unreal 엔진에서는 언리얼 엔진 모듈을 통해 가비지 컬렉션, 에디터 통합, 블루 프린트 기능 등을 제공해주고 있습니다.

하지만 사용자가 만든 객체들 즉, 사용자 정의 객체는 언리얼 엔진 모듈이 인식할 수 없습니다. 그래서 리플렉션을 통해 언리얼 엔진 모듈이 사용자 정의 객체를 인식하고 Unreal 엔진에서 제공하는 다양한 기능들과 상호 호환이 될 수 있도록 도와주는 것이 리플렉션 입니다.

 

사용자 정의 객체를 인식하고 Unreal 엔진의 기능들과 상호 호환이 될 수 있도록 하는데 사용하는 것이 UHT 코드 생성기 입니다.

사용자 정의 객체에 사용자가 리플렉션 매크로를 작성해주는 것으로 Unreal 엔진이 이 객체가 무엇인지 인지하고 우리의 코드를 이해합니다.

 

UCLASS()

C++ 클래스를 리플렉션 시스템에 등록하는 매크로 입니다.

클래스를 정의하기 전에 작성합니다.

 

UPROPERTY()

멤버 변수를 리플렉션 시스템에 등록하는 매크로 입니다.

멤버 변수를 선언하기 전에 작성합니다.

 

UFUNCTION()

멤버 함수를 리플렉션 시스템에 등록하는 매크로 입니다.

멤버 함수를 선언하기 전에 작성합니다.

 

USTRUCT()

C++ 구조체를 리플렉션 시스템에 등록하는 매크로 입니다.

구조체를 정의하기 전에 작성합니다.

 

GENERATED_BODY()

UHT가 생성하는 리플렉션 및 엔진 지원 코드를 위한 삽입 지점을 지정하는 매크로 입니다.

객체를 선언하고 정의할 때, 정의하는 내용 가장 앞에 작성합니다.

 

그 밖에도 괄호"( )" 안에 추가 설정을 넣어줘서 Blueprint에서 사용할 수 있게 설정하거나 다른 Unreal 엔진의 기능들과 호환이 가능하도록 설정이 가능합니다.

 

Unreal 엔진 리플렉션에 대해서는 나중에 더 알아보고 지금은 여기까지만 알아보겠습니다.

 

오버로딩(Overloading)

이전 글에서 오버로딩(Overloading)에 대해서 간단하게 짚고 넘어갔는데요.

이번 글에서는 조금 더 자세하게 어떤 방식으로 오버로딩이 작동하는지에 대해서 알아보도록 하겠습니다.

 

C 언어에서는 선언된 함수가 같은 함수인지 확인할 때, 함수의 이름만 가지고 판단합니다.

하지만 C++ 언어에서는 함수의 반환 타입(Return Type), 함수 이름(Function Name), 인자(Argument)를 가지고 비교하는 함수가 같은 함수인지 판단합니다. 그리고 반환 타입, 함수 이름, 인자 를 통해 고유 이름(제품의 고유 번호 같은 것)을 만드는 것을 네임 맹글링(Name Mangling)이라고 합니다.

 

오버로딩이 되는 조건은 (1) 매개변수의 타입이 다른 경우, (2) 매개변수의 개수가 다른 경우에 오버로딩을 할 수 있습니다.

 

반대로 오버로딩은 오버로딩된 함수가 어떤 형태로 호출되어야 하는지 명확하지 않고 애매모호한 경우 오버로딩을 할 수 없습니다.

(1) 타입 변환 가능한 매개변수로 인해 함수 호출이 애매모호한 함수 오버로딩 즉, void func(double x){}와 void func(float x){}로 오버로딩한 함수를 func(10);으로 호출하면 int 자료형의 타입 변환 우선순위는 double 자료형과 float 자료형이 같기 때문에 어떤 형태의 함수를 호출해야할지 애매모호 해집니다.

(2) 기본 매개변수로 인해 함수 호출 형태가 중복되는 함수 오버로딩 즉, void func(int x){}와 void func(int x, float y = 0.0f){}로 오버로딩한 함수를 func(10);으로 호출하면 두 형태의 함수가 모두 호출이 가능하여 어떤 형태의 함수를 호출해야할지 애매모호 해집니다.

(3) 매개변수의 타입만 포인터와 배열로 다른 함수 오버로딩 즉, void func(int* arr){}와 void func(int arr[]){}로 오버로딩한 함수를 func(dataArr);로 호출하면 포인터 변수와 배열은 모두 배열의 첫 번째 값 주소를 가지기 때문에 어떤 형태의 함수를 호출해야할지 애매모호 해집니다.

(4) 함수의 반환형만 다른 함수 오버로딩 즉, int func(int x){}와 float func(int x){}로 오버로딩한 함수를 func(10);으로 호출하면 어떤 형태로 값을 반환할 것인지 프로그램은 알 수 없기 때문에 애매모호 해집니다.

 

오버로딩된 함수는 어떤 절차로 호출할 함수를 선택하는지 학습하겠습니다.

컴파일러는 최대한 오류가 나지 않는 함수를 선택하려고 합니다. 같은 의미로 최대한 반환할 수 있는 함수를 선택하려고 합니다.

그래서 C++ 에서는 명확한 우선순위 규칙에 따라 호출할 함수를 선택합니다.

1. 정확한 매칭 - 말 그대로 인자의 자료형과 같은 자료형의 매개변수로 정의된 함수를 선택합니다.

2. 타입 승격 변환 - 인자의 값이 손실되지 않는 자료형으로 변환할 수 있는 함수를 선택합니다.

3. 표준 타입 변환 - 인자의 값이 손실되더라도 자료형을 변환할 수 있는 함수를 선택합니다.

4. 사용자 정의 타입 변환 - 클래스 타입의 변환 함수나 생성자에서 정의된대로 변환할 수 있는 함수를 선택합니다.

 

템플릿(Template)

타입에 관계없이 일반화된 코드를 작성하기 위해 사용하는 문법입니다.

템플릿은 어떤 자료형의 값이 올지 현재는 모르겠으나, 호출되는 자료형을 일반화해서 부르겠다는 의미를 갖습니다.

기본 형태는 template <typename T> 의 형태를 갖춥니다.

 

함수 오버로딩으로 작성하면 입력 받을 수 있는 자료형 형태로 모두 함수를 작성해주어야 하는 번거로움을 템플릿(Template)을 이용하면 짧고 간단하게 작성할 수 있습니다.

// 템플릿(Template)

#include <iostream>
#include <string>

using namespace std;

template <typename temp>
temp add(temp x, temp y)
{
    return x + y;
}

int main(void)
{
    int a = 2;
    int b = 7;

    cout << "a = " << a << "\nb = " << b << endl;
    cout << "a + b = " << add(a, b) << "\n" << endl;

    float c = 3.5f;
    float d = 4.3f;

    cout << "c = " << c << "\nd = " << d << endl;
    cout << "c + d = " << add(c, d) << "\n" << endl;

    string s1 = "Good";
    string s2 = "Morning";

    cout << "s1 = " << s1 << "\ns2 = " << s2 << endl;
    cout << "s1 + s2 = " << add(s1, s2) << "\n" << endl;

    return 0;
}

 

예문에서는 반환 값과 매개변수를 모두 템플릿(Template)을 사용하여 작성하였는데 모든 자료형을 템플릿으로 지정할 필요는 없습니다. void arraySize(T arr[], int size) { 함수 내용 생략 } 으로도 작성할 수 있습니다.

 

또한, 함수 뿐만아니라 클래스에서도 활용할 수 있습니다.

// 템플릿 클래스 (Template Class)

#include <iostream>
#include <string>

using namespace std;

template <typename T>
class Array
{
private:
    T array[10];
    int size;

public:
    Array(void) : size(0) {}

    void addValue(const T& value)
    {
        if (size < 10)
        {
            array[size++] = value;
        }
        else
        {
            cout << "Array is Full." << endl;
        }
    }

    void removeValue(void)
    {
        if (size > 0)
        {
            size--;
        }
    }

    void printArray(void)
    {
        cout << "{ ";

        for (int i = 0; i < size; i++)
        {
            if (i != (size - 1))
            {
                cout << array[i] << ", ";
            }
            else
            {
                cout << array[i] << " }" << endl;
            }
        }
    }
};

int main(void)
{
    Array<int> intArray;

    intArray.addValue(10);
    intArray.addValue(20);
    intArray.addValue(30);
    intArray.addValue(55);

    intArray.printArray();
    intArray.removeValue();
    intArray.printArray();

    Array<float> floatArray;

    floatArray.addValue(1.1);
    floatArray.addValue(2.2);
    floatArray.addValue(3.3);
    floatArray.addValue(5.5);

    floatArray.printArray();
    floatArray.removeValue();
    floatArray.printArray();

    Array<string> strArray;

    strArray.addValue("One");
    strArray.addValue("Two");
    strArray.addValue("Three");
    strArray.addValue("Five");

    strArray.printArray();
    strArray.removeValue();
    strArray.printArray();
}

클래스를 선언하기 전에 템플릿(template)을 이용하여 Array 클래스에서 배열의 원소로 받을 자료형을 일반화하여 어떤 자료형이 와도 호환이 되도록 구현하였습니다.

 

오늘은 여기까지 학습하였습니다!

내일도 힘내서 열심히 공부하도록 하겠습니다.