C++ Basic

[C++ Basic] Reference, Template, Allocate

서론

c++ basic을 공부하기 위해서는 다른 언어들과의 기본적인 차이를 이해해야 한다. 그 차이는 바로 메모리 관리와 원본 변수를 다루고 주소를 다룬다는 점에 있다. 우리는 앞으로 C++의 이러한 개념들에 대해서 학습해볼 것이다,

C++ Basics: 정적 할당과 동적 할당의 차이

매우 기본적인 개념이라 모르면 안되지만 그래도 헷갈릴 수 있는 부분이라 정리를 해보겠다.

정적할당과 동적할당은 컴파일과 관련이 있는데, 컴파일 시점에서 메모리 크기와 위치가 정해지면 정적할당이고 런타임중에 지정되면 동적할당이다.

참고로 정적할당은 스택에 저장, 동적할당은 힙에 저장되기 때문에 함수가 종료되어도 메모리 상에 남아있어 무조건 해제하는 과정을 해줘야 한다.

int main() {
    int n;
    cout << "배열 크기 입력: ";
    cin >> n;

    // 동적으로 배열 할당
    int *arr = new int[n];

    // 배열 채우기
    for (int i = 0; i < n; i++) {
        arr[i] = i * 10;
    }

    // 배열 출력
    for (int i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;

    // 동적 할당 해제
    delete[] arr;
    // 값 타입이면 delete

    return 0;
}

런타임에서 배열의 크기를 바꾸는 것은 불가능 하기 때문에 동적할당으로 사용자에게 입력을 받아 크기를 정해줘야 한다. 정적할당으로는 사용자에게 입력을 받아 배열의 크기를 바꾸는 것이 불가능 하다.
⚠️ delete[]를 이용해 꼭 메모리 해제를 시켜줘야 한다. – 배열형태로 메모리 할당을 했기 때문에 배열 형태로 해제해야함.
일반적인 변수는 delete로 해제함.

함수와 구조체

함수와 구조체는 뭐 당연히 알지만 C++은 무엇인가. 메모리를 다루는 언어 아닌가? 그래서 함수에 값을 전달하는 방식에 포인터를 이용해 값을 바꿀 수 있었다.

함수 매개변수

void changeValue(int *ptr) {
    ptr += 5;
}

int main() {
    int num = 10;
    std::cout << "변경 전 num: " << num << std::endl; // 10

    changeValue(&num);  // num의 주소값을 넘겨줌

    std::cout << "변경 후 num: " << num << std::endl; // 15

    return 0;
}

이렇게 값 타입의 주소를 넘겨 원하는대로 수정할 수 있다.

void changeValue(ref int number) {
    number++;
}
int x = 5;
changeValue(ref x);  // x는 이제 6이 됨
void changeValue(out int a) {
    a = 10;
}
int y;
changeValue(out y);  // y는 이제 10

참고로 C#에서도 ref와 out 키워드를 이용해 값을 변경할 수 있는데, ref는 초기화를 꼭 해줘야 한다는 것이고 out은 안해도 된다는 점이다.

구조체

struct Person{
    string name;
    int age;
    float height;
}

이런식으로 구조체를 만들었으면 이를 접근해야하는데

Person adult;
adult.name = "yoon";
adult.age = 24;

이렇게 정적할당이 되어있으면 구조체 안의 개별 멤버는 .을 이용해 접근할 수 있다.

Person *adult = new Person;
adult -> name = "yoon2"
adult -> age = 23;

동적 할당은 ->을 이용해 접근해야 한다.

Person* arr = new Person[3];
arr[0].name = "Alice"; // . 연산자 사용
arr[1].age = 25;

참고로 배열요소에 접근할때는 정적,동적 둘다 .을 사용한다.

상수

C#에서는 const와 readonly를 이용해서 상수를 만들 수 있는데, c++에서도 마찬가지로 const를 이용해 상수를 만들 수 있다. (readonly는 불가능)
-> 레퍼런스 변수

포인터 변수와 상수화

int main() {
    int a = 2;
    const int *ptr = &a;

    a = 1; // 컴파일 통과
    *ptr = 10; // 오류
}
  • 포인터 변수를 상수화 하면 역참조 연산으로는 값을 변경할 수 없다.
int main() {
    int a = 2;
    int b = 1;
    int *const ptr = &a;

    a = 10; // 컴파일 통과
    ptr = &b; // 오류
}
  • 포인터 변수 자체를 상수화 하면 포인터 변수가 참조하는 변수를 변경할 수 없다.

Reference 변수

메모리 주소가 아닌 원본 변수를 참조하는 것.
c#에서 ref와 out이 참조 변수 즉, 레퍼번스 변수이다.
레퍼런스 변수를 표현할때 변수 이름 앞에 &를 붙이는 경우가 있는데, 이러면 포인터의 주소를 가르키는 문법과 혼동하기 쉽기 때문에 주로 타입바로 뒤에 &를 붙이는걸로 구분하는게 정석이다.

// 매개변수를 ref변수로 선언
void swap(int& ref_a, int& ref_b) {
    int temp = ref_a;
    ref_a = ref_b;
    ref_b = temp;
}

int main() {
    int a = 5;
    int b = 10;
    swap(a,b);
    cout << "a: " << a << ", b: " << b << endl; // 10,5
    return 0;
}

이렇게 하면 원본 변수가 매개변수로 전달이 되어 swap이 잘 일어난다.

int& ref_a; // error

참고로 레퍼번스 변수는 선언할 때 참조할 변수를 선언하지 않으면 오류가 발생한다.

int a = 10;
int& ref_a = a;
int b = 20;
ref_a = b;

레퍼런스 변수는 다른 변수를 참조하게 수정할 수 없기 때문에 이렇게 작성하면 참조하는 변수가 바뀌는 것이 아니라 a의 값이 20이 된다.

예외 처리

사실 C#이 매우 편한 언어였음을 C++을 배우면서 느낀다. 예외 처리를 하기 위해서는 try, throw, catch가 있어야 한다는 건 알고 있을 것이다. 하지만 c++은 throw를 무조건 작성해야 예외가 동작한다.

try {
    int[] arr = {1,2,3};
    Console.WriteLine(arr[10]); // 자동으로 예외 발생
} catch (Exception e) {
    Console.WriteLine(e.Message); // catch에서 처리됨
}

C#에서는 이렇게 throw를 따로 명시적으로 작성하지 않아도, 런타임에서 Exception이 알아서 날아갔다… 고맙다 C#…. 하지만 우리의 C++은 그렇지 않다.

try {
    int a = 10, b = 0;
    int c = a / b; // 0으로 나누기 (런타임 에러)
    cout << c << endl;
} catch (const char* msg) {
    cout << "예외 발생: " << msg << endl;
}

이 c++코드는 catch가 동작하지 않고 프로그램이 비정상적으로 종료되게 된다. 이유는 throw가 없어서 예외를 처리하지 못하기 때문이다.

try {
    int a = 10, b = 0;
    if (b == 0) throw "0으로 나눌 수 없습니다!";
    int c = a / b;
    cout << c << endl;
} catch (const char* msg) {
    cout << "예외 발생: " << msg << endl;
}

이렇게 오류가 나올 수 있는 부분에 throw를 통해 예외를 처리해 줘야 한다.

C++ 신택스 및 표준 라이브러리에 대한 자세한 내용은 cppreference 공식 문서를 참고하면 좋다.

스택 풀기

C++에서 예외가 발생했을 때, 예외가 처리될 때까지 호출 스택을 “풀며” 각 함수의 변수와 객체를 정리하는 과정이다.

void func3() { throw std::runtime_error("예외 발생!"); }
void func2() { func3(); }
void func1() { func2(); }

int main() {
    try {
        func1();
    } catch (std::exception& e) {
        std::cout << e.what() << std::endl;
    }
}

func3에서 예외가 발생하게 되면 func3() -> func2() -> func1() -> main 순으로 스택이 풀리면서 각 함수가 정리된다.

예외 생략

void safeFunction() noexcept {
    // 예외를 발생시키지 않는 코드
    std::cout << "This function will not throw exceptions." << std::endl;
}

noexcept 키워드를 통해 특정 함수가 예외를 던지지 않는다고 명시적으로 표시할 수 있다. 이렇게 되면 최적화 측면에서 이점이 생기게 된다.
하지만 만약 예외가 발생한다면 바로 terminate()가 호출이 되면서 프로그램이 즉시 종료된다.

void myTerminate() {
    std::cerr << "Unhandled exception! Program will terminate." << std::endl;
    exit(-1); // 반드시 종료
}

int main() {
    std::set_terminate(myTerminate);
    throw 1; // 처리되지 않은 예외
}

set_terminate는 c++에서 “처리되지 않은 예외”가 발생해 프로그램이 종료될 때, 기본 종료 동작을 대체하는 함수이다.
따라서 set_terminate는 항상 프로그램을 종료하는 로직이 들어가야 한다.
종료 코드로는 abort(), exit(), quick_exit()등이 있다.

Template

함수 템플릿은 C#의 제너릭과 비슷한 개념으로 자료형을 하나로 일반화해서 쓰는 문법이다.

template <typeName T>
T Add (T a1, T a2) {
    return a1 + a2;
}

int main() {
    int    x = Add(1, 2);        // T = int 로 자동 추론
    float  y = Add(1.5f, 2.5f);  // T = float
    double z = Add(1.2, 3.4);    // T = double
}

이렇게 여러 자료형을 동시에 커버할 수 있다.

명시적 호출

  • 이는 컴파일러가 타입을 자동으로 추론할 수 있게 두지 않고 직접 타입을 적는 방법이다.
template <typename T>
T Add(T a, T b) { return a + b; }

// 1) 자동 추론(암시적 호출)
int    a = Add(1, 2);          // T = int
double b = Add(1.2, 3.4);      // T = double

// 2) 명시적 호출
int    c = Add<int>(1, 2);     // T = int 을 강제로 지정
auto   d = Add<double>(1, 2);  // 인자는 int지만 T는 double로 계산

템플릿 특수화

템플릿 특수화는 기본 템플릿은 그대로 두고 특정 타입에 대해서만 다른 버전으로 만드는 것을 의미한다.

// 1. 기본 함수 템플릿
template <typename T>
void LogDamage(T dmg) {
    std::cout << "[DMG] " << dmg << "\n";
}

// 2. int에 대한 특수화
template <>
void LogDamage<int>(int dmg) {
    std::cout << "[DMG:int] " << dmg << " (critical)\n";
}

이렇게 특수화 하고 싶은 자료형을 따로 해두면 된다.
특수화를 하면 기본 템플릿 대신 특수화 부분이 실행이 되게 된다.

public static class Logger
{
    public static void Log<T>(T v)
    {
        Console.WriteLine($"[GENERIC] {v}");
    }

    // int 전용 오버로드
    public static void Log(int v)
    {
        Console.WriteLine($"[INT] {v} (critical)");
    }
}

C#에서도 이런식으로 제너릭을 특수화 시킬 수 있다.

클래스 템플릿

템플릿으로 범용 클래스를 만들면 이는 클래스 템플릿이 된다. 클래스를 템플릿으로 만들경우 멤버 변수, 멤버 함수는 템플릿으로 처리한다.
(무조건 템플릿일 필요는 없음)

template <typename T>
class PlayerStat {
public:
    T hp;                  // 템플릿 타입
    int level;             // 일반 타입
    static int onlineCnt;  // static도 가능

    void AddHp(T v) {      // 템플릿 타입 사용하는 멤버
        hp += v;
    }

    void LevelUp() {       // 템플릿과 무관한 일반 멤버 함수
        level++;
    }
};

template <typename T>
int PlayerStat<T>::onlineCnt = 0;

이렇게 클래스 자체를 템플릿화 한다.

PlayerStat    p1;  // T = int
PlayerStat  p2;  // T = float

그리고 클래스 템플릿은 처음 선언을 할때 자료형 타입 또한 같이 명시해줘야 한다.

클래스 템플릿 부분 특수화

template <typename T1, typename T2>
class Pair {
public:
    T1 first;
    T2 second;
};

// T1 = int, T2는 아무 타입(T) 가능
template <typename T>
class Pair<int, T> {
public:
    int first;
    T   second;

    void Print() {
        std::cout << "[int, T] " << first << ", " << second << "\n";
    }
};

이렇게 특정 부분만 특수화를 진행할 수 있다.

// 기본: 어떤 타입이든 그냥 복사해서 패킷 생성
template <typename T>
class PacketWriter {
public:
    void Write(const T& data) {
        // data를 그대로 직렬화
    }
};

// 부분 특수화: 포인터 타입만 특별 처리 (널 체크 등)
template <typename T>
class PacketWriter<T*> {
public:
    void Write(T* data) {
        if (data == nullptr) {
            // 널 포인터면 '없음' 패킷
        } else {
            // *data를 직렬화해서 TCP로 전송
        }
    }
};

참고로 포인터 타입으로 특수화를 진행할 수도 있다.

lyh0309

View Comments

Recent Posts

[C++] Lock, Mutex, condition variable

Race Condition 서로 다른 Thread에서 같은 메모리를 공유할 때 같은 자원을 사용하면서 값이 이상하게 되는…

3 weeks ago

[C++ Basic] Move Semantics, Lvalue, Rvalue

서론 c++을 이용해서 개발을 하다보면 복사 대신에 이동연산을 통해서 여러 이점을 얻는다는 정보를 확인할 수…

4 weeks ago

[C++ Basic] Pointer에 대하여

서론 C++에서 진짜 너무 많이 등장하는 개념이 바로 Pointer이다. 메모리의 주소를 관리하고~ 뭐시기~ 항상 등장하는데,…

1 month ago

[C++ Basic] Casting

서론 이번시간에는 Casting이라는 개념에 대해서 학습해 보도록 하겠다. 하지만 그 전에 Composition과 Aggregation의 차이에 대해서…

2 months ago