12.1 클래스 템플릿
템플릿
함수 템플릿, 클래스 템플릿
- C++의 템플릿을 이용하면 함수나 클래스를 정의할 때 특정 데이터 형을 사용하는 대신 범용형을 사용할 수 있다.
- 함수 템플릿이나 클래스 템플릿은 여러가지 데이터 형에 대해서 함수 정의나 클래스 정의를 생성할 수 있다.
템플릿 장점과 제네릭 프로그래밍
템플릿 장점
- 함수 코드의 재사용
- 높은 소프트웨어의 생산성과 유용성
템플릿 단점
- 포팅에 취약
- 컴파일러에 따라 지원하지 않을 수 있음
- 컴파일 오류 메시지 빈약, 디버깅에 많은 어려움
제네릭 프로그래밍
- generic programming
- 일반화 프로그래밍이라고도 부름
- 제네릭 함수나 제네릭 클래스를 활용하는 프로그래밍 기법
- C++에서 STL(Standard Template Library) 제공, 활용
- 보편화 추세
- Java, C# 등 많은 언어에서 활용
클래스 템플릿의 선언
제네릭 클래스 만들기
template <class 범용형이름, ...> // template <typename 범용형이름, ...>
class 클래스이름
{
// 멤버 변수 정의
// 멤버 함수 정의
};
클래스 템플릿 멤버 함수 외부 정의
template <class T=int> // 아무것도 지정 안하면 int로 된다.
class Stack{
protected:
int m_size;
int m_top;
T *m_buffer;
public:
Stack(int size = sz);
~Stack();
void Push(T value);
T Pop();
// ...
};
template <class T>
void Stack<T>::Push(T Value){
// ...
}
template <class T>
T Stack<T>::Pop(){
// ...
}
// ...
int main(){
Stack<int> iStack; // int 타입을 다루는 스택 객체 생성
Stack<double> dStack; // double 타입을 다루는 스택 객체 생성
iStack.Push(3);
int n = iStack.Pop();
dStack.Push(3.5);
double d = dStack.Pop();
}
클래스 템플릿 사용 예
개발자가 만든 코드
template<class A, class B, int MAX>
class TwoArray{
// 생략
A arr1[MAX];
B arr2[MAX];
};
TwoArray<char, double, 20> arr;
컴파일러에 의해 생성된 클래스
// char, double, 20 을 템플릿 파라미터로 주었을 때 가정하고 클래스 만든 것
class TwoArray_char_double_20{
// 중간 생략
char arr1[20];
char arr2[20];
};
클래스 템플릿의 인스턴스화
항상 명시적으로 지정
- 객체를 생성할 때, 템플릿의 파라미터 지정
Stack<int> s1(10); // T는 int 형
Stack<string> s2(5); // T는 string 형
- 객체를 생성하지는 않지만 객체에 대한 포인터나 레퍼런스를 정의하면서 템플릿의 파라미터 지정
Stack<char> *pStack = NULL; // T는 char 형
- typedef 문으로 클래스 템플릿의 별명을 정의하면서 템플릿의 파라미터 지정
typedef Stack<Point> PointStack; // T는 Point 형
PointStack s4(20); // Stack<Point> 클래스의 객체 생성
클래스 템플릿의 사용
어디에서든지 사용 가능
void f1(Stack<int>& s); // 함수 인자로 사용
Stack<double>* f2(); // 함수 리턴형으로 사용
class X{
protected:
Stack<string> m_strStack; // 멤버 객체의 데이터 형으로 사용
// ...
};
class MyStack : public Stack<int>{ // ... }; // 다른 클래스의 기본 클래스로 사용
template<typename T>
class Array{
protected:
T arr[3];
// ...
};
Array<Stack<int>> arra; // 다른 클래스 템플릿 파라미터로 사용
템플릿의 특징
실제로 사용되기 전까지는 코드가 생성되지 않음
- 클래스 템플릿을 인스턴스화 하는 코드를 작성하면, 컴파일러가 해당 문장을 컴파일할 때 클래스의 코드를 생성함.
- 컴파일 시간이 오래 걸리는 단점이 있다. (하지만 크게 문제가 될 정도는 아님)
코드 크기를 최소화 함
- 미리 코드를 만들어두는 것이 아니라, 프로그램에 실제로 사용되는 클래스나 함수에 대해서만 코드가 생성됨
함수 템플릿의 정의나, 클래스 템플릿의 멤버 함수의 정의는 헤더 파일에 놓여야 함
- 컴파일 시간에 클래스나 함수를 만들어내기 위해서는 헤더 파일에 위치할 필요가 있다.(컴파일 시간에는 다른 구현 파일의 내용을 확인할 수 없다.)
2개의 제네릭 타입을 가진 클래스 만들기
#include <iostream>
using namespace std;
template <class T1, class T2> // 2개의 제네릭 타입 선언
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('m', 12.5);
y.get(c, d);
cout<<"c="<<c<<'\t'<<"d="<<d<<endl;
}
/*
a=2 b=0.5
c=m d=12.5
*/
12.2 STL
C++ 표준 템플릿 라이브러리, STL
STL = Standard Template Library
- 표준 템플릿 라이브러리
- C++ 표준 라이브러리 중 하나
- 대부분의 C++ 컴파일러는 STL을 지원
- 성능이 우수하고 안전성이 검증된 라이브러리
- 많은 템플릿 클래스(제네릭 클래스)와 템플릿 함수(제네릭 함수) 포함
- 개발자는 이들을 이용하여 쉽게 응용 프로그램 작성
- 기간도 단축하고 최소한의 노력만으로 원하는 기능을 구현 가능
STL의 구성
STL의 구성
- 컨테이너 - 템플릿 클래스
- 데이터를 담아두는 자료 구조를 표현한 클래스
- 리스트, 큐, 스택, 맵, 셋, 벡터
- iterator - 컨테이너 원소에 대한 포인터
- 컨테이너의 원소들을 순회하면서 접근하기 위해 만들어진 컨테이너 원소에 대한 포인터
- 알고리즘 - 템플릿 함수
- 컨테이너 원소에 대한 복사, 검색, 삭제, 정렬 등의 기능을 구현한 템플릿 함수
- 컨테이너의 멤버 함수 아님
STL 컨테이너
컨테이너 클래스 | 설명 | 헤더 파일 |
vector | 동적 크기의 배열을 일반화한 클래스 | <vector> |
deque | 앞뒤 모두 입력 가능한 큐 클래스 | <deque> |
list | 빠른 삽입/삭제 가능한 리스트 클래스 | <list> |
set | 정렬된 순서로 값을 저장하는 집합 클래스, 값은 유일 | <set> |
map | (key, value) 쌍으로 값을 저장하는 맵 클래스 | <map> |
stack | 스택을 일반화한 클래스 | <stack> |
queue | 큐를 일반화한 클래스 | <queue> |
STL과 관련된 헤더 파일고 이름 공간
헤더파일
- 컨테이너 클래스를 사용하기 위한 헤더 파일
- 해당 클래스가 선언된 헤더 파일 include 예) vector 클래스: #include <vector>
- 알고리즘 함수를 사용하기 위한 헤더 파일
- 알고리즘 함수에 상관 없이 #include <algorithm>
이름 공간
- STL이 선언된 이름 공간은 std
iterator 사용
iterator란?
- 반복자라고도 부름
- 컨테이너의 원소를 가리키는 포인터
iterator 변수 선언
- 구체적인 컨테이너를 지정하여 반복자 변수 생성
- begin()과 end() 함수
- STL 컨테이너가 공통으로 제공하는 함수
- begin() 함수: 첫 번째 원소를 가리키는 iterator 리턴
- end() 함수: 마지막 원소를 가리키는 iterator 리턴
iterator 사용 에
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<int> v; // 정수 벡터 생성
v.push_back(1);
v.push_back(2);
v.push_back(3);
vector<int>::iterator it; // 벡터 v의 원소에 대한 포인터 it 선언
for(it = v.begin(); it!=v.end(); it++){ // iterator를 이용하여 모든 원소 탐색
int n = *it; // it가 가리키는 원소 값 리턴
n = n*2;
*it = n; // it가 가리키는 원소에 값 쓰기
}
for(it = v.begin(); it!=v.end(); it++)
cout<<*it<<' ';
cout<<endl;
}
/*
2 4 6
*/
map 사용하기
특징
- (키, 값) 쌍을 원소로 저장하는 제네릭 컨테이너
- 동일한 키를 가진 원소가 중복 저장되면 오류 발생
- '키'로 '값' 검색
- 많은 응용에서 필요함
- #include <map> 필요
맵 컨테이너 생성 예
map<string, string> dc;
// 원소 저장
dic.insert(make_pair("love", "사랑"));
dic["love"] = "사랑";
// 원소 검색
string kor = dic["love"];
string kor = dic.at("love");
map 클래스의 주요 멤버와 연산자
멤버와 연산자 함수 | 설명 |
insert(pair<> &element) | 맵에 '키'와 '값;으로 구성된 pair 객체 element 삽입 |
at(key_type& key) | 맵에서 '키' 값에 해당하는 '값' 리턴 |
begin() | 맵의 첫 번째 원소에 대한 참조 리턴 |
end() | 맵의 끝(마지막 원소 다음)을 가리키는 참조 리턴 |
empty() | 맵이 비어 있으면 true 리턴 |
find(key_type& key) | 맵에서 '키' 값에 해당하는 원소를 가리키는 iterator 리턴 |
erase(iterator it) | 맵에서 it가 가리키는 원소 삭제 |
size() | 맵에 들어 있는 원소의 개수 리턴 |
operator[key_type& key]() | 맵에서 '키' 값에 해당하는 원소를 찾아 '값' 리턴 |
operator=() | 맵 치환(복사) |
STL 컨테이너의 장점
STL 컨테이너의 종류가 다르더라도 각 컨테이너가 제공하는 인터페이스는 동일
- list 컨테이너에서 사용했던 push_back, back, begin, end 등의 멤버 함수는 list, vector, deque 등의 다른 STL 컨테이너에서도 동일하게 제공됨
#include <vector>
vector<int> list1; // vector로 변경해도 프로그램의 나머지 부분은 그대로 동작
최소한의 코드 수정으로 컨테이너를 다른 것으로 바꿀 수 있음
=> 프로그램의 개발이나 유지 보수 시간을 단축하는 데 큰 도움이 됨
STL 알고리즘
STL이 제공하는 범용 함수
- 템플릿 함수
- 전역 함수
- STL 컨테이너 클래스의 멤버 함수가 아님
- iterator와 함께 작동
copy | merge | random | rotate |
equal | min | remove | search |
find | move | replace | sort |
max | partition | reverse | swap |
sort() 함수 사례
- 두 개의 매개 변수
- 첫 번째 매개 변수: 소팅을 시작한 원소의 주소
- 두 번째 매개 변수: 소팅 범위의 마지막 원소 다음 주소
vector<int> v;
// ...
sort(v.begin(), v.begin()+3); // v.begin()에서 v.begin()+2까지, 처음 3개 원소 정렬
sort(v.begin()+2, v.begin()+5); // 벡터 3번째 원소에서 v.begin()+4까지, 3개 원소 정렬
sort(v.begin(), v.end()); // 벡터 전체 정렬
auto를 이용하여 쉬운 변수 선언
C++에서 auto
- 기능
- C++ 11 부터 auto 선언의 의미 수정: 컴파일러에게 변수선언문에서 추론하여 타입을 자동 선언하도록 지시
- C++ 11 이전까지는 스택에 할당되는 지역 변수를 선언하는 키워드
- 장점
- 복잡한 변수 선언을 간소하게, 긴 타입 선언 시 오타 줄임
auto의 기본 사용 사례
auto pi = 3.14; // 3.14가 실수이므로 pi는 double 타입으로 선언됨
auto n = 3; // 3이 정수이므로 n을 int 타입으로
auto *p = &n; // 변수 p는 int* 타입으로 추론
int n = 10;
int& ref = n; // ref는 int에 대한 참조 변수
auto ref2 = ref; // ref2는 int& 변수로 자동 선언
auto의 다른 활용 사례
다른 활용 사례
- 함수의 리턴 타입으로부터 추론하여 변수 타입 선언
int square(int x) {return x*x;}
// ...
auto ret = square(3); // 변수 ret는 int 타입으로 추론
- STL 템플릿에 활용
- vector<int> iterator 타입의 변수 it를 auto를 이용하여 간단히 선언
vector<int>::iterator it;
for(it = v.begin(); it!=v.end(); it++)
cout<<*it<<endl;
for(auto it = v.begin(); it!=v.end(); it++)
cout<<*it<<endl;
제네릭(generic) 프로그래밍
제네릭 프로그래밍
- 정보의 타입과 정보를 처리하는 알고리즘을 분리하여 서로 독립적으로 관리할 수 있는 프로그래밍 방식
- 컨테이너: '정보의 타입'
- sort() 함수를 비롯한 알고리즘 함수: '정보를 처리하는 알고리즘'
제네릭 프로그래밍의 목표
- 정보의 타입과 관계없이 동일한 방법으로 처리할 수 있는 알고리즘을 구현하는 것
제네릭 프로그래밍의 장점
타입과 알고리즘간의 불필요한 연관성 제거(decoupling)
- vector 클래스의 세부 구현을 변경했다고 해도 sort() 함수는 영향을 받지 않음
재사용성(reusability) 증가
확장의 용이성
- 공통적인 인터페이스: begin(), end() 등
- 새로운 컨테이너 클래스를 만들 때 이 공통의 인터페이스를 유지한다면 STL의 모든 알고리즘을 사용할 수 있다.
-- -종강 ---
'공부 > C++' 카테고리의 다른 글
11. 가상함수와 추상클래스 (0) | 2024.06.02 |
---|---|
10. 상속 (0) | 2024.05.27 |
9. friend와 연산자 중복 (0) | 2024.05.16 |
8. 함수와 참조, 복사생성자 (1) | 2024.05.14 |
7. 여러가지 객체의 생성방법 (0) | 2024.05.14 |