Skip to main content

Command Palette

Search for a command to run...

C언어 (11)

Updated
6 min read

1. 템플릿

1-1. 템플릿의 정의

템플릿(Template)은 타입을 매개변수로 받아서 함수나 클래스를 자동으로 생성하는 틀 이다. "어떤 타입이든 동작하는 코드"를 한 번만 작성하면, 컴파일러가 실제 사용되는 타입에 맞춰 코드를 찍어낸다.

1-2. 템플릿의 필요성

int 두 개를 비교해서 큰 값을 반환하는 함수를 만들었다고 하자.

int getMax(int a, int b) {
    return (a > b) ? a : b;
}

그런데 double에도 같은 함수가 필요하다. string에도 필요하다. 로직은 완전히 같은데 타입만 다르다.

int getMax(int a, int b) { return (a > b) ? a : b; }
double getMax(double a, double b) { return (a > b) ? a : b; }
string getMax(string a, string b) { return (a > b) ? a : b; }

오버로딩으로 해결할 수는 있지만, 본질적으로 같은 코드를 타입만 바꿔서 반복 작성 하는 것이다. 새로운 타입이 추가될 때마다 함수를 하나 더 만들어야 한다. 이 문제를 해결하는 것이 템플릿이다.

1-3. 템플릿의 구문 구조

템플릿은 template 키워드와 꺾쇠(< >) 안에 타입 매개변수 를 지정해서 만든다.

template <typename T>

T는 관례적으로 사용하는 이름이지만, Type, U, DataType 등 아무 이름이나 쓸 수 있다. typename 대신 class를 써도 동일하게 동작한다. template <class T>template <typename T>는 같은 의미다.

타입 매개변수는 여러 개 사용할 수도 있다.

template <typename T, typename U>

1-4. 함수 템플릿

함수 템플릿은 타입에 독립적인 함수 를 만든다. 아까의 getMax 함수를 템플릿으로 만들면 이렇다.

template <typename T>
T getMax(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    cout << getMax(10, 20) << endl;            // T가 int로 추론
    cout << getMax(3.14, 2.72) << endl;        // T가 double로 추론
    cout << getMax<string>("abc", "xyz") << endl;  // 명시적으로 타입 지정
    return 0;
}

getMax(10, 20)을 호출하면 컴파일러가 인자의 타입을 보고 Tint추론 한다. 그리고 int getMax(int a, int b) 함수를 자동으로 생성한다. getMax(3.14, 2.72)를 호출하면 double 버전이 자동으로 만들어진다.

꺾쇠로 타입을 직접 지정할 수도 있다. getMax<string>("abc", "xyz")처럼 쓰면 "T를 string으로 쓰겠다"고 명시하는 것이다.

중요한 점은 템플릿 자체가 함수가 아니라 함수를 만들어내는 틀 이라는 것이다. 실제 함수는 특정 타입으로 호출될 때 컴파일러가 생성한다. 이 과정을 템플릿 인스턴스화(instantiation) 라고 한다.

여러 타입 매개변수를 사용하는 예시도 보자.

template <typename T, typename U>
void printPair(T first, U second) {
    cout << first << ", " << second << endl;
}

int main() {
    printPair(1, 3.14);         // T=int, U=double
    printPair("Hello", 42);     // T=const char*, U=int
    return 0;
}

1-5. 클래스 템플릿

클래스 템플릿은 타입에 독립적인 클래스 를 만든다. 어떤 타입의 데이터든 담을 수 있는 범용 컨테이너를 만들 때 유용하다.

template <typename T>
class Box {
private:
    T value;

public:
    Box(T v) : value(v) {}

    T getValue() {
        return value;
    }

    void setValue(T v) {
        value = v;
    }
};

int main() {
    Box<int> intBox(42);
    Box<string> strBox("Hello");
    Box<double> dblBox(3.14);

    cout << intBox.getValue() << endl;   // 42
    cout << strBox.getValue() << endl;   // Hello
    cout << dblBox.getValue() << endl;   // 3.14
    return 0;
}

함수 템플릿과 달리 클래스 템플릿은 타입을 명시적으로 지정해야 한다. Box<int>, Box<string>처럼 꺾쇠 안에 타입을 넣어서 객체를 생성한다.

좀 더 실용적인 예시로, 간단한 스택을 템플릿으로 만들어보자.

template <typename T>
class Stack {
private:
    T *data;
    int top;
    int capacity;

public:
    Stack(int cap) : capacity(cap), top(-1) {
        data = new T[capacity];
    }

    ~Stack() {
        delete[] data;
    }

    void push(T value) {
        if (top < capacity - 1) {
            data[++top] = value;
        }
    }

    T pop() {
        if (top >= 0) {
            return data[top--];
        }
        throw "스택이 비어있습니다";
    }

    bool isEmpty() {
        return top == -1;
    }
};

int main() {
    Stack<int> intStack(10);
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);
    cout << intStack.pop() << endl;  // 3

    Stack<string> strStack(5);
    strStack.push("Hello");
    strStack.push("World");
    cout << strStack.pop() << endl;  // World

    return 0;
}

Stack<int>는 정수를 담는 스택이 되고, Stack<string>은 문자열을 담는 스택이 된다. 코드는 하나지만 타입에 따라 다른 클래스가 자동 생성된다. JavaScript의 배열이 push()/pop()으로 스택처럼 동작하는 것과 비슷하지만, C++ 템플릿은 타입 안전성까지 보장한다.

실제로 C++ 표준 라이브러리(STL)의 vector, stack, queue, map 같은 컨테이너들이 전부 클래스 템플릿으로 만들어져 있다. vector<int>, vector<string> 형태로 쓰는 것이 바로 클래스 템플릿의 인스턴스화다.


2. 예외처리

프로그램 실행 중에는 예상치 못한 상황이 발생할 수 있다. 0으로 나누기, 메모리 할당 실패, 배열 범위 초과, 파일 열기 실패 등이 그렇다. 이런 상황을 예외(Exception) 라 하고, 이를 처리하는 메커니즘이 예외처리 다.

C에서는 반환값으로 에러를 알렸다. 함수가 -1이나 NULL을 반환하면 에러인 식이다. 이 방식은 반환값을 매번 체크해야 하고, 정상 반환값과 에러 반환값을 구분하기 어려울 때가 있다.

C++에서는 try, catch, throw 키워드로 예외를 처리한다.

try {
    // 예외가 발생할 수 있는 코드
} catch (예외타입 변수) {
    // 예외 처리 코드
}

throw는 예외를 던지는 것이고, try 블록 안에서 발생한 예외를 catch 블록이 잡는 것이다.

double divide(int a, int b) {
    if (b == 0) {
        throw "0으로 나눌 수 없습니다";  // 예외 던지기
    }
    return (double)a / b;
}

int main() {
    try {
        cout << divide(10, 2) << endl;   // 5
        cout << divide(10, 0) << endl;   // 여기서 예외 발생
        cout << "이 줄은 실행되지 않음" << endl;
    } catch (const char *msg) {
        cout << "에러: " << msg << endl;  // 에러: 0으로 나눌 수 없습니다
    }

    cout << "프로그램 계속 실행" << endl;
    return 0;
}

예외가 발생하면 try 블록의 나머지 코드는 건너뛰고 바로 catch 블록으로 점프한다. 예외처리 후 프로그램은 catch 블록 이후부터 정상적으로 계속 실행된다.

여러 종류의 예외를 다르게 처리할 수도 있다.

try {
    // ...
} catch (int e) {
    cout << "정수 에러 코드: " << e << endl;
} catch (const char *msg) {
    cout << "메시지: " << msg << endl;
} catch (...) {
    cout << "알 수 없는 예외" << endl;  // 모든 예외를 잡음
}

catch (...)는 앞의 catch에서 잡지 못한 모든 종류의 예외를 잡는다. 마지막에 두는 것이 일반적이다.

실무에서는 문자열이나 정수 대신 예외 클래스 를 정의해서 사용한다.

class DivideByZeroException {
private:
    string message;

public:
    DivideByZeroException(string msg) : message(msg) {}

    string getMessage() const {
        return message;
    }
};

double divide(int a, int b) {
    if (b == 0) {
        throw DivideByZeroException("0으로 나눌 수 없습니다");
    }
    return (double)a / b;
}

int main() {
    try {
        divide(10, 0);
    } catch (const DivideByZeroException &e) {
        cout << e.getMessage() << endl;
    }
    return 0;
}

C++ 표준 라이브러리는 <stdexcept> 헤더에 runtime_error, invalid_argument, out_of_range 같은 예외 클래스를 제공한다. 이들은 모두 exception 클래스를 상속받는다.

#include <stdexcept>

double divide(int a, int b) {
    if (b == 0) {
        throw runtime_error("0으로 나눌 수 없습니다");
    }
    return (double)a / b;
}

try {
    divide(10, 0);
} catch (const exception &e) {
    cout << e.what() << endl;  // what()으로 메시지 확인
}

예외처리의 핵심 원칙은 정상 흐름과 에러 처리를 분리 하는 것이다. try 블록에는 정상적인 로직만 두고, 에러 대응은 catch 블록에 모아둔다. 코드의 가독성이 높아지고, 에러 처리를 빼먹을 위험도 줄어든다.


마무리

템플릿은 타입에 독립적인 코드를 작성하게 해주는 C++의 강력한 기능이다. 같은 로직을 타입만 바꿔서 반복 작성하는 문제를 해결하며, STL의 모든 컨테이너가 클래스 템플릿으로 만들어져 있다. 예외처리는 try/catch/throw로 정상 흐름과 에러 처리를 깔끔하게 분리한다. C의 반환값 기반 에러 처리보다 직관적이고 안전하다.

More from this blog

C언어 (13)

1. STL의 개념 1-1. 배경 C++로 프로그래밍을 하다 보면 동적 배열, 연결 리스트, 정렬, 검색 같은 자료구조와 알고리즘을 반복적으로 구현하게 된다. 프로젝트마다 매번 새로 만들면 시간도 낭비되고, 버그가 생길 가능성도 높아진다. 이런 문제를 해결하기 위해 자주 사용되는 자료구조와 알고리즘을 미리 만들어서 표준 라이브러리에 포함 시킨 것이 STL이

Apr 1, 202610 min read3

chamdom's tech

16 posts