10장 템플릿과 표준 템플릿 라이브러리(STL) 명품 C++ 10장 템플릿과 표준 템플릿 라이브러리(STL)
학습 목표 일반화와 템플릿의 개념과 목적을 이해한다. 템플릿으로부터 구체화의 과정을 이해한다. 제네릭(템플릿) 함수를 작성하고 활용할 수 있다. 제네릭(템플릿) 클래스를 작성하고 활용할 수 있다. C++ 표준 템플릿 라이브러리(STL)에 대해 이해하고, 활용할 수 있다. vector 컨테이너를 이해하고 활용하는 연습을 통해 다른 STL 컨테이너를 활용할 수 있 다. iterator에 대해 이해하고 간단히 활용할 수 있다.
두 함수는 매개 변수만 다르고 나머지 코드는 동일함 함수 중복의 약점 – 중복 함수의 코드 중복 #include <iostream> using namespace std; void myswap(int& a, int& b) { int tmp; tmp = a; a = b; b = tmp; } void myswap(double & a, double & b) { double tmp; int main() { int a=4, b=5; myswap(a, b); // myswap(int& a, int& b) 호출 cout << a << '\t' << b << endl; double c=0.3, d=12.5; myswap(c, d); // myswap(double& a, double& b) 호출 cout << c << '\t' << d << endl; 두 함수는 매개 변수만 다르고 나머지 코드는 동일함 동일한 코드 중복 작성 5 4 12.5 0.3
일반화와 템플릿 제네릭(generic) 또는 일반화 템플릿 템플릿 선언 함수나 클래스를 일반화시키고, 매개 변수 타입을 지정하여 틀에서 찍어 내듯이 함수나 클래스 코드를 생산하는 기법 템플릿 함수나 클래스를 일반화하는 C++ 도구 template 키워드로 함수나 클래스 선언 변수나 매개 변수의 타입만 다르고, 코드 부분이 동일한 함수를 일반화시킴 제네릭 타입 - 일반화를 위한 데이터 타입 템플릿 선언 템플릿을 선언하는 키워드 제네릭 타입을 선언하는 키워드 제네릭 타입 T 선언 template <class T> 또는 template <typename T> 3 개의 제네릭 타입을 가진 템플릿 선언 template <class T1, class T2, class T3> template <class T> void myswap (T & a, T & b) { T tmp; tmp = a; a = b; b = tmp; } 템플릿을 이용한 제네릭 함수 myswap
중복 함수들로부터 템플릿 만들기 사례 template <class T> void myswap(int & a, int & b) { int tmp; tmp = a; a = b; b = tmp; } 템플릿을 선언하는 키워드 제네릭 타입을 선언하는 키워드 제네릭 타입 T 선언 template <class T> void myswap (T & a, T & b) { T tmp; tmp = a; a = b; b = tmp; } 제네릭 함수 만들기(일반화) void myswap (double & a, double & b) { double tmp; tmp = a; a = b; b = tmp; } 템플릿을 이용한 제네릭 함수 중복 함수들
myswap(a, b) 호출에 필요한 함수 구체화 진행 템플릿으로부터의 구체화 구체화(specialization) 템플릿의 제네릭 타입에 구체적인 타입 지정 템플릿 함수로부터 구체화된 함수의 소스 코드 생성 T에 int를 대입하여 구체화된 소스 코드 생성 template <class T> void myswap(T & a, T & b) { T tmp; tmp = a; a = b; b = tmp; } void myswap(int & a, int & b) { int tmp; tmp = a; a = b; b = tmp; } int main() { int a=4, b=5; myswap(a, b); 구체화 컴파일 후 실행 호출 int main() { int a=4, b=5; myswap(a, b); } myswap(a, b) 호출에 필요한 함수 구체화 진행
제네릭 함수로부터 구체화된 함수 생성 사례 int a=4, b=5; myswap(a, b); void myswap(int & a, int & b) { int tmp; tmp = a; a = b; b = tmp; } T -> int template <class T> void myswap(T & a, T & b) { T tmp; tmp = a; a = b; b = tmp; } double c=0.3, d=12.5; myswap(c, d); void myswap(double & a, double & b) { double tmp; tmp = a; a = b; b = tmp; } T ->double char e=‘a’, f=‘k’; myswap(e, f); 제네릭 함수 void myswap(char & a, char & b) { char tmp; tmp = a; a = b; b = tmp; } T -> char 구체화 구체화된 버전의 C++ 소스 생성
예제 10–1 제네릭 myswap() 함수 만들기 #include <iostream> int main() { using namespace std; class Circle { int radius; public: Circle(int radius=1) { this->radius = radius; } int getRadius() { return radius; } }; template <class T> void myswap(T & a, T & b) { T tmp; tmp = a; a = b; b = tmp; } int main() { int a=4, b=5; myswap(a, b); cout << "a=" << a << ", " << "b=" << b << endl; double c=0.3, d=12.5; myswap(c, d); cout << "c=" << c << ", " << "d=" << d << endl; Circle donut(5), pizza(20); myswap(donut, pizza); cout << "donut반지름=" << donut.getRadius() << ", "; cout << "pizza반지름=" << pizza.getRadius()<< endl; } myswap(int& a, int& b) 함수 구체화 및 호출 myswap(double& a, double& b) 함수 구체화 및 호출 myswap(Circle& a, Circle& b) 함수 구체화 및 호출 a=5, b=4 c=12.5, d=0.3 donut반지름=20, pizza반지름=5
구체화 오류 제네릭 타입에 구체적인 타입 지정 시 주의 두 매개 변수 a, b의 제네릭 타입 동일 template <class T> void myswap(T & a, T & b) int s=4; double t=5; myswap(s, t); 컴파일 오류. 템플릿으로부터 myswap(int &, double &) 함수를 구체화할 수 없다. 두 개의 매개 변수의 타입이 서로 다름
템플릿 장점과 제네릭 프로그래밍 템플릿 장점 템플릿 단점 제네릭 프로그래밍 함수 코드의 재사용 포팅에 취약 높은 소프트웨어의 생산성과 유용성 템플릿 단점 포팅에 취약 컴파일러에 따라 지원하지 않을 수 있음 컴파일 오류 메시지 빈약, 디버깅에 많은 어려움 제네릭 프로그래밍 generic programming 일반화 프로그래밍이라고도 부름 제네릭 함수나 제네릭 클래스를 활용하는 프로그래밍 기법 C++에서 STL(Standard Template Library) 제공. 활용 보편화 추세 Java, C# 등 많은 언어에서 활용
예제 10–2 큰 값을 리턴하는 bigger() 함수 만들기 연습 #include <iostream> using namespace std; template <class T> T bigger(T a, T b) { // 두 개의 매개 변수를 비교하여 큰 값을 리턴 if(a > b) return a; else return b; } int main() { int a=20, b=50; char c='a', d='z'; cout << "bigger(20, 50)의 결과는 " << bigger(a, b) << endl; cout << "bigger('a', 'z')의 결과는 " << bigger(c, d) << endl; bigger(20, 50)의 결과는 50 bigger('a', 'z')의 결과는 z
예제 10–3 배열의 합을 구하여 리턴하는 제네릭 add() 함수 만들기 연습 #include <iostream> using namespace std; template <class T> T add(T data [], int n) { // 배열 data에서 n개의 원소를 합한 결과를 리턴 T sum = 0; for(int i=0; i<n; i++) { sum += data[i]; } return sum; // sum와 타입과 리턴 타입이 모두 T로 선언되어 있음 int main() { int x[] = {1,2,3,4,5}; double d[] = {1.2, 2.3, 3.4, 4.5, 5.6, 6.7}; cout << "sum of x[] = " << add(x, 5) << endl; // 배열 x와 원소 5개의 합을 계산 cout << "sum of d[] = " << add(d, 6) << endl; // 배열 d와 원소 6개의 합을 계산 sum of x[] = 15 sum of d[] = 23.7
예제 10-4 배열을 복사하는 제네릭 함수 mcopy() 함수 만들기 연습 #include <iostream> using namespace std; // 두 개의 제네릭 타입 T1, T2를 가지는 copy()의 템플릿 template <class T1, class T2> void mcopy(T1 src [], T2 dest [], int n) { // src[]의 n개 원소를 dest[]에 복사하는 함수 for(int i=0; i<n; i++) dest[i] = (T2)src[i]; // T1 타입의 값을 T2 타입으로 변환한다. } int main() { int x[] = {1,2,3,4,5}; double d[5]; char c[5] = {'H', 'e', 'l', 'l', 'o'}, e[5]; mcopy(x, d, 5); // int x[]의 원소 5개를 double d[]에 복사 mcopy(c, e, 5); // char c[]의 원소 5개를 char e[]에 복사 for(int i=0; i<5; i++) cout << d[i] << ' '; // d[] 출력 cout << endl; for(int i=0; i<5; i++) cout << e[i] << ' '; // e[] 출력 mcopy()의 T1은 int로, T2는 double로 구체화 mcopy()의 T1, T2 모두 char로 구체화 1 2 3 4 5 H e l l o
배열을 출력하는 print() 템플릿 함수의 문제점 #include <iostream> using namespace std; template <class T> void print(T array [], int n) { for(int i=0; i<n; i++) cout << array[i] << '\t'; cout << endl; } int main() { int x[] = {1,2,3,4,5}; double d[5] = { 1.1, 2.2, 3.3, 4.4, 5.5 }; print(x, 5); print(d, 5); char c[5] = {1, 2, 3, 4, 5}; print(c, 5); char로 구체화되면 숫자대신 문자가 출력되는 문제 발생! T가 char로 구체화되는 경우, 정수 1, 2, 3, 4, 5에 대한 그래픽 문자 출력 print() 템플릿의 T가 int 타입으로 구체화 print() 템플릿의 T가 char 타입으로 구체화
예제 10-5 템플릿 함수보다 중복 함수가 우선 #include <iostream> using namespace std; template <class T> void print(T array [], int n) { for(int i=0; i<n; i++) cout << array[i] << '\t'; cout << endl; } void print(char array [], int n) { // char 배열을 출력하기 위한 함수 중복 cout << (int)array[i] << '\t'; // array[i]를 int 타입으로 변환하여 정수 출력 int main() { int x[] = {1,2,3,4,5}; double d[5] = { 1.1, 2.2, 3.3, 4.4, 5.5 }; print(x, 5); print(d, 5); char c[5] = {1,2,3,4,5}; print(c, 5); 템플릿 함수와 중복된 print() 함수 중복된 print() 함수가 우선 바인딩 템플릿 print() 함수로부터 구체화 1 2 3 4 5 1.1 2.2 3.3 4.4 5.5 주목
제네릭 클래스 만들기 제네릭 클래스 선언 제네릭 클래스 구현 클래스 구체화 및 객체 활용 template <class T> class MyStack { int tos; T data [100]; // T 타입의 배열 public: MyStack(); void push(T element); T pop(); }; 제네릭 클래스 선언 제네릭 클래스 구현 클래스 구체화 및 객체 활용 template <class T> void MyStack<T>::push(T element) { ... } template <class T> T MyStack<T>::pop() { MyStack<int> iStack; // int 타입을 다루는 스택 객체 생성 MyStack<double> dStack; // double 타입을 다루는 스택 객체 생성 iStack.push(3); int n = iStack.pop(); dStack.push(3.5); double d = dStack.pop();
예제 10–6 제네릭 스택 클래스 만들기 3 3.5 a #include <iostream> using namespace std; template <class T> class MyStack { int tos;// top of stack T data [100]; // T 타입의 배열. 스택의 크기는 100 public: MyStack(); void push(T element); // element를 data [] 배열에 삽입 T pop(); // 스택의 탑에 있는 데이터를 data[] 배열에서 리턴 }; MyStack<T>::MyStack() { // 생성자 tos = -1; // 스택은 비어 있음 } void MyStack<T>::push(T element) { if(tos == 99) { cout << "stack full"; return; tos++; data[tos] = element; T MyStack<T>::pop() { T retData; if(tos == -1) { cout << "stack empty"; return 0; // 오류 표시 retData = data[tos--]; return retData; int main() { MyStack<int> iStack; // int 만 저장하는 스택 iStack.push(3); cout << iStack.pop() << endl; MyStack<double> dStack; // double 만 저장하는 스택 dStack.push(3.5); cout << dStack.pop() << endl; MyStack<char> *p = new MyStack<char>(); // char만 저장하는 스택 p->push('a'); cout << p->pop() << endl; delete p; } 3 3.5 a
예제 10–7 제네릭 스택의 제네릭 타입을 포인터나 클래스로 구체화하는 예 #include <iostream> #include <string> using namespace std; /* 이 부분에 예제 10-6에 작성한 MyStack 템플릿 클래스 코드가 생략되었음 */ 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; } }; int main() { MyStack<int *> ipStack; // int* 만을 저장하는 스택 int *p = new int [3]; for(int i=0; i<3; i++) p[i] = i*10; // 0, 10, 20으로 초기화 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 객체 저장 스택 Point a(2,3), b; pointStack.push(a); // Point 객체 a 푸시. 복사되어 저장 b = pointStack.pop(); // Point 객체 팝 b.show(); // Point 객체 출력 MyStack<Point*> pStack; // Point* 포인터 스택 pStack.push(new Point(10,20)); // Point 객체 푸시 Point* pPoint = pStack.pop(); // Point 객체의 포인터 팝 pPoint->show(); // Point 객체 출력 MyStack<string> stringStack; // 문자열만 저장하는 스택 string s="c++"; stringStack.push(s); stringStack.push("java"); cout << stringStack.pop() << ' ‘; cout << stringStack.pop() << endl; } 0 10 20 (2,3) (10,20) java c++
참고!! cout << a << b << c;의 실행 순서 cout으로 << 연산자가 연속되면, 마지막 데이터부터 거꾸로 cout의 스택에 삽입한 후 앞에서부터 << 연산자를 순서대로 처리한다. 이때 스택에 삽입된 데이터를 팝하여 사용한다. cout << 1 << 2 << 3; 123 3 cout 스택 3 cout 스택 2 3 cout 스택 2 1 3 cout 스택 2 3 cout 스택 cout 스택 스택 톱의 값 1로 cout << 1 실행. 1 출력 스택 톱의 값 2로 cout << 2 실행. 2 출력 스택 톱의 값 3로 cout << 3 실행. 3 출력 예제 10-7의 라인 39-40을 다음과 같이 연결하면, 잘못된 결과가 나오며, 이유를 설명하면 다음과 같다. 맨 뒤에 있는 stringStack.pop() 함수를 호출하여 리턴 값을 cout의 스택에 삽입하고, ‘ ‘ 문자를 cout 스택에 삽입하고, 다시 맨 앞이 있는 stringStack.pop() 함수를 호출하여 리턴 값을 cout의 스택에 삽입한 후, 앞에서부터 << 연산자를 순서대로 처리한다. cout << stringStack.pop() << ' ' << stringStack.pop(); c++ java ""java" cout 스택 "java" cout 스택 ' ' "java" cout 스택 ' ' "c++" "java" cout 스택 ' ' "java" cout 스택 cout 스택 스택 톱에 있는 "c++" 출력 스택 톱에 있는 ' ' 출력 스택 톱에 있는 "java" 출력
예제 10–8 두 개의 제네릭 타입을 가진 클래스 만들기 #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; } void GClass<T1, T2>::set(T1 a, T2 b) { data1 = a; data2 = b; 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; } data1을 a에, data2를 b에 리턴하는 함수 a=2 b=0.5 c=m d=12.5
C++ 표준 템플릿 라이브러리, STL STL(Standard Template Library) STL의 구성 많은 제네릭 클래스와 제네릭 함수 포함 개발자는 이들을 이용하여 쉽게 응용 프로그램 작성 STL의 구성 컨테이너 – 템플릿 클래스 데이터를 담아두는 자료 구조를 표현한 클래스 리스트, 큐, 스택, 맵, 셋, 벡터 iterator – 컨테이너 원소에 대한 포인터 컨테이너의 원소들을 순회하면서 접근하기 위해 만들어진 컨테이너 원소 에 대한 포인터 알고리즘 – 템플릿 함수 컨테이너 원소에 대한 복사, 검색, 삭제, 정렬 등의 기능을 구현한 템플릿 함수 컨테이너의 멤버 함수 아님
STL과 관련된 헤더 파일과 이름 공간 헤더파일 이름 공간 컨테이너 클래스를 사용하기 위한 헤더 파일 해당 클래스가 선언된 헤더 파일 include 예) vector 클래스를 사용하려면 #include <vector> list 클래스를 사용하려면 #include <list> 알고리즘 함수를 사용하기 위한 헤더 파일 알고리즘 함수에 상관 없이 #include <algorithm> 이름 공간 STL이 선언된 이름 공간은 std
vector 컨테이너 특징 가변 길이 배열을 구현한 제네릭 클래스 원소의 저장, 삭제, 검색 등 다양한 멤버 함수 지원 개발자가 벡터의 길이에 대한 고민할 필요 없음 원소의 저장, 삭제, 검색 등 다양한 멤버 함수 지원 벡터에 저장된 원소는 인덱스로 접근 가능 인덱스는 0부터 시작
vector 클래스의 주요 멤버와 연산자
vector 다루기 사례
예제 10–9 vector 컨테이너 활용하기 #include <iostream> #include <vector> using namespace std; int main() { vector<int> v; // 정수만 삽입 가능한 벡터 생성 v.push_back(1); // 벡터에 정수 1 삽입 v.push_back(2); // 벡터에 정수 2 삽입 v.push_back(3); // 벡터에 정수 3 삽입 for(int i=0; i<v.size(); i++) // 벡터의 모든 원소 출력 cout << v[i] << " "; // v[i]는 벡터의 i 번째 원소 cout << endl; v[0] = 10; // 벡터의 첫 번째 원소를 10으로 변경 int n = v[2]; // n에 3이 저장 v.at(2) = 5; // 벡터의 3 번째 원소를 5로 변경 } 1 2 3 10 2 5
예제 10-10 문자열을 저장하는 벡터 만들기 연습 string 타입의 vector를 이용하여 문자열을 저장하는 벡터를 만들고, 5개의 이름을 입력 받아 사전에 서 가장 뒤에 나오는 이름을 출력하라 #include <iostream> #include <string> #include <vector> using namespace std; int main() { vector<string> sv; // 문자열 벡터 생성 string name; cout << "이름을 5개 입력하라" << endl; for(int i=0; i<5; i++) { // 한 줄에 한 개씩 5 개의 이름을 입력받는다. cout << i+1 << ">>"; getline(cin, name); sv.push_back(name); } name = sv.at(0); // 벡터의 첫 원소 for(int i=1; i<sv.size(); i++) { if(name < sv[i]) // sv[i]의 문자열이 name보다 사전에서 뒤에 나옴 name = sv[i]; // name을 sv[i]의 문자열로 변경 cout << "사전에서 가장 뒤에 나오는 이름은 " << name << endl; 이름을 5개 입력하라 1>>황기태 2>>이재문 3>>김남윤 4>>한원선 5>>애슐리 사전에서 가장 뒤에 나오는 이름은 황기태
it는 원소가 int 타입인 벡터의 원소에 대한 포인터 iterator 사용 iterator란? 반복자라고도 부름 컨테이너의 원소를 가리키는 포인터 iterator 변수 선언 구체적인 컨테이너를 지정하여 반복자 변수 생성 vector<int>::iterator it; it = v.begin(); it는 원소가 int 타입인 벡터의 원소에 대한 포인터 vector<int> v; it ...... 1 2 3 4 5 4 9 v.begin() v.end()
예제 10-11 iterator를 사용하여 vector의 모든 원소에 2 곱하기 #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; // 곱하기 2 *it = n; // it가 가리키는 원소에 값 쓰기 } for(it=v.begin(); it != v.end(); it++) // 벡터 v의 모든 원소 출력 cout << *it << ' '; cout << endl; 2 4 6
STL 알고리즘 사용하기 알고리즘 함수 sort() 함수 사례 템플릿 함수 전역 함수 iterator와 함께 작동 두 개의 매개 변수 첫 번째 매개 변수 : 소팅을 시작한 원소의 주소 두 번째 매개 변수 : 소팅 범위의 마지막 원소 다음 주소 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()); // 벡터 전체 정렬
예제 10-12 sort() 함수를 이용한 vector 소팅 #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> v; // 정수 벡터 생성 cout << "5개의 정수를 입력하세요>> "; for(int i=0; i<5; i++) { int n; cin >> n; v.push_back(n); // 키보드에서 읽은 정수를 벡터에 삽입 } // v.begin()에서 v.end() 사이의 값을 오름차순으로 정렬 // sort() 함수의 실행 결과 벡터 v의 원소 순서가 변경됨 sort(v.begin(), v.end()); vector<int>::iterator it; // 벡터 내의 원소를 탐색하는 iterator 변수 선언 for(it=v.begin(); it != v.end(); it++) // 벡터 v의 모든 원소 출력 cout << *it << ' '; cout << endl; 주목 5개의 정수를 입력하세요>> 30 -7 250 6 120 -7 6 30 120 250