위 포스팅에서 제너릭의 개념과 이를 위한 template 의 사용법과 예제를 확인했습니다. 맨 마지막에 제네릭 클래스에 대해 다루긴 했는데요 너무 간단하게 다룬 것 같아 오늘은 예제 위주로 확인해보며 template 를 이용한 제네릭 클래스에 대해 더 자세히 알아보는 포스팅입니다.
제네릭 클래스를 만들기 위해서는, 클래스의 선언부와 구현부를 모두 template 로 선언해야 합니다. 제네릭을 이용한 스택 클래스 코드를 보면서 자세히 확인해보도록 하겠습니다.
class Point {
int x, y;
public:
Point(int x = 0, int y = 0) { this->x = x; this->y = y; }
void show() { cout << '(' << x << ',' << y << ')' << endl; }
};
template <class T>
class MyStack {
int tos;
T data[100];
public:
MyStack();
void push(T element);
T pop();
};
template <class T>
MyStack<T>::MyStack() { tos = -1; }
template <class T>
void MyStack<T>::push(T element) {
if (tos == 99) {
cout << "stack full";
return;
}
data[++tos] = element;
}
template <class T>
T MyStack<T>::pop() {
T retData;
if (tos == -1) {
cout << "stack empty";
return 0;
}
retData = data[tos--];
return retData;
}
먼저 기본 클래스인 Point 클래스와 이를 상속 받은 MyStack 클래스가 있습니다. 스택 클래스는 data의 타입을 정해놓지 않고 제너릭 타입 T를 사용하기 때문에 선언 위에 template <class T> 를 사용합니다. 그리고 push 와 pop 또한 원형만 선언했는데 이 두 멤버 함수도 제너릭 타입을 사용합니다.
여기서 빼먹기가 쉬운데, 클래스 외부에서 멤버 함수를 구현할 때에도 제너릭 타입을 쓰는 경우 template <class T> 를 사용해야 한다는 점입니다. 이 때문에 선언부와 구현부 모두 template 선언 이라고 위에서 말한 겁니다.
이렇게 제너릭 타입으로 선언한 클래스를 구체화 하는 방법은 클래스이름<자료형> 입니다. 제네릭 클래스를 이용할 때는 클래스의 이름과 함께 제네릭 타입에 적용할 구체적인 타입을 필수적으로 지정해줘야 합니다.
int main() {
MyStack<int*> ipStack;
int* p = new int[3];
for (int i = 0; i < 3; i++) p[i] = i * 10;
ipStack.push(p);
int* q = ipStack.pop();
for (int i = 0; i < 3; i++) cout << q[i] << ' ';
cout << endl;
delete[] p;
MyStack<Point> pointStack;
Point a(2, 3), b;
pointStack.push(a);
b = pointStack.pop();
b.show();
MyStack<Point*> pStack;
pStack.push(new Point(10, 20));
Point* pPoint = pStack.pop();
pPoint->show();
MyStack<string> stringStack;
string s = "c++";
stringStack.push(s);
stringStack.push("java");
cout << stringStack.pop() << ' ';
cout << stringStack.pop() << endl;
}
다양한 예에 대해 MyStack 클래스의 객체를 생성해 사용하는 코드입니다. 보면 Point 타입 객체로도 선언하고, int*, string , Point* 등 다양한 자료형에 대해 스택 클래스를 사용하고 있습니다. 아래는 전체 코드 및 실행 결과입니다.
#include <iostream>
#include <string>
using namespace std;
class Point {
int x, y;
public:
Point(int x = 0, int y = 0) { this->x = x; this->y = y; }
void show() { cout << '(' << x << ',' << y << ')' << endl; }
};
template <class T>
class MyStack {
int tos;
T data[100];
public:
MyStack();
void push(T element);
T pop();
};
template <class T>
MyStack<T>::MyStack() { tos = -1; }
template <class T>
void MyStack<T>::push(T element) {
if (tos == 99) {
cout << "stack full";
return;
}
data[++tos] = element;
}
template <class T>
T MyStack<T>::pop() {
T retData;
if (tos == -1) {
cout << "stack empty";
return 0;
}
retData = data[tos--];
return retData;
}
int main() {
MyStack<int*> ipStack;
int* p = new int[3];
for (int i = 0; i < 3; i++) p[i] = i * 10;
ipStack.push(p);
int* q = ipStack.pop();
for (int i = 0; i < 3; i++) cout << q[i] << ' ';
cout << endl;
delete[] p;
MyStack<Point> pointStack;
Point a(2, 3), b;
pointStack.push(a);
b = pointStack.pop();
b.show();
MyStack<Point*> pStack;
pStack.push(new Point(10, 20));
Point* pPoint = pStack.pop();
pPoint->show();
MyStack<string> stringStack;
string s = "c++";
stringStack.push(s);
stringStack.push("java");
cout << stringStack.pop() << ' ';
cout << stringStack.pop() << endl;
}
2개 이상의 제너릭 타입을 가지는 경우는 template <class T1, class T2, ... > 와 같은 방식으로 여러 개의 제너릭 타입을 선언하여 사용할 수 있습니다.
// 두 개의 제네릭 타입 사용하는 제네릭 클래스
#include <iostream>
using namespace std;
template <class T1, class T2>
class GClass {
T1 data1;
T2 data2;
public:
GClass();
void set(T1 a, T2 b);
void get(T1& A, T2& B);
};
template <class T1, class T2>
GClass<T1, T2>::GClass() {
data1 = 0;
data2 = 0;
}
template <class T1, class T2>
void GClass<T1, T2>::set(T1 a, T2 b) {
data1 = a;
data2 = b;
}
template <class T1, class T2>
void GClass<T1, T2>::get(T1& A, T2& B) {
A = data1;
B = data2;
}
int main() {
int a;
double b;
GClass <int, double> x;
x.set(2, 0.5);
x.get(a, b);
cout << "a= " << a << "\t" << "b= " << b << endl;
char c;
float d;
GClass <char, float> y;
y.set('s', 4.2);
y.get(c, d);
cout << "c= " << c << "\t" << "d= " << d << endl;
}
마지막으로 연산자 오버로딩 및 friend 로 멤버를 선언할 때 제너릭 타입을 적용하는 예시입니다.
#include <iostream>
using namespace std;
template <typename T>
class Point{
T x, y;
public:
Point(T a = 0, T b = 0) :x(a), y(b) { }
template <typename T>
friend ostream& operator<<(ostream& out, Point<T>& Po);
};
template <typename T>
ostream& operator<<(ostream& out, Point<T>& Po)
{
out << "(" << Po.x << ", " << Po.y << ")" << endl;
return out;
}
int main() {
Point<int> a(3, 5);
cout << a << endl;
Point<double> b(3.4, 5.5);
cout << b << endl;
}
중간에 아주 중요한게 friend 키워드를 사용할 때 만약 제너릭 타입을 매개변수로 사용한다면, 외부 구현부 뿐만 아니라 선언부 내부에도 추가적으로 template <typename T> 를 써줘야만 합니다. 저 한 줄이 없다면 컴파일 오류가 발생합니다.
즉 제너릭 클래스의 경우 구현부와 선언부 모두에 template <class 이름> 이나 template <typename 이름> 을 사용해주어야 컴파일러가 제대로 인식합니다. 제네릭 클래스의 경우 헷갈리기도 하고, 또 연산자 오버로딩이나 프렌드의 경우에도 섞이면 많이 헷갈리기 때문에 구현부와 선언부 모두 라는 것을 잘 기억하시면 될 것 같습니다. 감사합니다.
'C++ > C++ 기초' 카테고리의 다른 글
[C++] STL ( pair, vector, list, iterator, algorithm 사용 예시 코드 ) (1) | 2023.07.18 |
---|---|
[C++] 순수 가상 함수와 추상 클래스 ( Pure virtual function and Abstract class ) (0) | 2023.07.17 |
[C++] 파생 클래스와 기본 클래스의 생성자/소멸자 호출 관계 + 가상 소멸자 (0) | 2023.07.17 |
[C++] 가상 함수와 오버라이딩 ( Virtual function and overriding ) (0) | 2023.07.17 |
[C++] 상속 (Inheritance) - 업캐스팅/다운캐스팅 (Up/Down casting) (0) | 2023.07.17 |