Search
Duplicate

10장. 템플릿과 표준 템플릿 라이브러리 (STL)

생성일
2022/05/09 08:20
태그
C++

10장. 템플릿과 표준 템플릿 라이브러리 (STL)

함수 중복의 약점 - 중복 함수의 코드 중복

#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; tmp = a; a = b; b = tmp; } // 동일한 코드 중복 작성 // 두 함수는 매개변수만 다르고 나머지 코드는 동일함 int main() { int a = 4, b = 5; myswap(a, b); cout << a << '\t' << b << endl; double c = 0.3, d = 12.5; myswap(c, d); cout << c << '\t' << d << endl; }
C++
복사

일반화와 템플릿

제네릭 (generic) 또는 일반화

함수나 클래스를 일반화시키고, 매개변수 타입을 지정하여
틀에서 찍어내듯이
함수나 클래스 코드르 생산하는 기법

템플릿

함수나 클래스를 일반화하는 C++ 도구
template 키워드로 함수나 클래스 선언
변수나 매개변수의 타입만 다르고, 코드 부분이 동일한 함수를 일반화시킴
제네릭 타입 - 일반화를 위한 데이터 타입

템플릿 선언

template <class T> 또는 template <typename T> // 3개의 제네릭 타입을 가진 템플릿 선언 template <class T1, class T2, class T3>
C++
복사
template <class T> // 제네릭 타입 T 선언 void myswap(T& a, T& b) { T tmp; tmp = a; a = b; b = tmp; }
C++
복사

구체화 (specialization)

템플릿의 제네릭 타입에, 구체적인 타입을 지정
템플릿 함수로부터 구체화된 함수의 소스코드 생성
template <class T> // 제네릭 타입 T 선언 void myswap(T& a, T& b) { T tmp; tmp = a; a = b; b = tmp; }
C++
복사
이 코드를
void myswap(int& a, int& b) { int tmp; tmp = a; a = b; b = tmp; }
C++
복사
로 구체화

제네릭 myswap() 함수 만들기

#include <iostream> 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; }
C++
복사

구체화 오류

제네릭 타입에 구체적인 타입 지정 시 주의
template <class T> void myswap(T& a, T& b)
C++
복사
→ 두 매개변수 a, b의 제네릭 타입 동일
int s = 4; double t = 5; myswap(s, t);
C++
복사
→ 두 개의 매개변수의 타입이 서로 다름
→ 컴파일 오류, 템플릿으로부터 myswap(int &, double &) 함수를 구체화할 수 없다

템플릿 장점과 제네릭 프로그래밍

템플릿 장점

함수 코드의 재사용
높은 SW의 생산성과 유용성

템플릿 단점

포팅에 취약
컴파일러에 따라 지원하지 않을 수 있음
컴파일 오류 메시지 빈약, 디버깅에 많은 어려움

제네릭 프로그래밍 (generic programming)

일반화 프로그래밍
제네릭 함수나 제네릭 클래스를 활용하는 프로그래밍 기법
C++ 에서 STL (Standard Template Library) 제공, 활용
보편화 추세 - JAVS, 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; }
C++
복사
예제 10-3) 배열의 합을 구하여 리턴하는 제네릭 add() 함수 만들기 연습
#include <iostream> using namespace std; template <class T> T add(T data [], int n) { T sum = 0; for (int i = 0; i < n; i++) { sum += data[i]; } return sum; } 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; cout << "sum of d[] = " << add(d, 6) << endl; }
C++
복사
예제 10-4) 배열을 복사하는 제네릭 함수 mcopy() 함수 만들기 연습
#include <iostream> using namespace std; // 두 개의 제네릭 타입 T1, T2를 가지는 copy() 의 템플릿 template <class T1, class T2> void mcopy(T1 src [], T2 dest [], int n) { 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[] 출력 cout << endl; }
C++
복사
배열을 출력하는 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'; // T가 char로 구체화되는 경우, 정수 1, 2, 3, 4, 5에 대한 그래픽 문자 출력 // char로 구체화되면, 숫자대신 문자가 출력되는 문제 발생 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() 템플릿의 T가 int 타입으로 구체화 print(d, 5); char c[5] = {1, 2, 3, 4, 5}; print(c, 5); // print() 템플릿의 T가 char 타입으로 구체화 }
C++
복사
예제 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; } // 템플릿 함수와 중복된 print() 함수 void print(char array [], int n) { // char 배열을 출력하기 위한 함수 중복 for (int i = 0; i < n; i++) cout << (int)array[i] << '\t'; // array[i]를 int 타입으로 변환하여 정수 출력 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); // 템플릿 print() 함수로부터 구체화 char c[5] = {1, 2, 3, 4, 5}; print(c, 5); // 중복된 print() 함수가 우선 바인딩 }
C++
복사

제네릭 클래스 만들기

제네릭 클래스 선언

template <class T> class MyStack { int tos; T data[100]; // T 타입의 배열 public: MyStack(); void push(T element); T pop(); };
C++
복사

제네릭 클래스 구현

template <class T> void MyStack<T>::push(T element) { ... } tmeplate <class T> T MyStack<T>::pop() { ... }
C++
복사

클래스 구체화 및 객체 활용

MyStack<int> iStack; // int 타입을 다루는 스택 객체 생성 MyStack<double> dStack; // double 타입을 다루는 스택 객체 생성 iStack.push(3); int n = iStack.pop(); dStack.push(3.5); double d = dStack.pop();
C++
복사
예제 10-6) 제네릭 스택 클래스 만들기
#include <iostream> using namespace std; template <class T> class MyStack { int tos; T data [100]; // T 타입의 배열, 스택의 크기는 100 public: MyStack(); void push(T element); // element를 data [] 배열에 삽입 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; } tos++; 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> 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; }
C++
복사
예제 10-7) 제네릭 스택의 제네릭 타입을 포인터나 클래스로 구체화하는 예
#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; } }; 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(); MyStack<Point*> pStack; // Point* 포인터 스택 pStack.push(new Point(10, 20)); // Point 객체 푸시 Point* pPoint = pStack.pop(); // Point 객체의 포인터 팝 pPoint->show(); MyStack<string> stringStack; // 문자열만 저장하는 스택 string s = "c++"; stringStack.push(s); stringStack.push("java"); cout << stringStack.pop() << ' '; cout << stringStack.pop() << endl; }
C++
복사
예제 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); // data1을 a에, data2를 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; }
C++
복사

C++ 표준 템플릿 라이브러리, STL

STL (Standard Template Library)

표준 템플릿 라이브러리
많은 제네릭 클래스와 제네릭 함수 포함

STL의 구성

컨테이너 - 템플릿 클래스
데이터를 담아두는 자료구조를 표현한 클래스
리스트, 큐, 스택, 맵, 셋, 벡터
iterator - 컨테이너 원소에 대한 포인터
컨테이너의 원소들을 순회하면서, 접근하기 위해 만들어진 컨테이너 원소에 대한 포인터
알고리즘 - 템플릿 함수
컨테이너 원소에 대한 복사, 검색, 삭제, 정렬 등의 기능을 구현한 템플릿 함수
컨테이너의 멤버 함수 아님

STL 컨테이너의 종류

컨테이너 클래스
설명
헤더 파일
vector
가변 크기의 배열을 일반화한 클래스
<vector>
deque
앞 뒤 모두 입력 가능한 큐 클래스
<deque>
list
빠른 삽입/삭제 가능한 리스트 클래스
<list>
set
정렬된 순서로 값을 저장하는 집합 클래스, 값은 유일
<set>
map
(key, value) 쌍을 저장하는 맵 클래스
<map>
stack
스택을 일반화한 클래스
<stack>
queue
큐를 일반화한 클래스
<queue>

STL iterator의 종류

iterator의 종류
iterator에 ++ 연산 후 방향
read/write
iterator
다음 원소로 전진
read/write
const_iterator
다음 원소로 전진
read
reverse_iterator
지난 원소로 후진
read/write
const_reverse_iterator
지난 원소로 후진
read

STL 알고리즘 함수들

copy
merge
random
rotate
equal
min
remove
search
find
move
replace
sort
max
partition
reverse
swap

vector 컨테이너

가변 길이 배열을 구현한 제네릭 클래스 - 벡터의 길이에 대한 고민할 필요 없다!
원소의 저장, 삭제, 검색 등 다양한 멤버 함수 지원
벡터에 저장된 원소는 인덱스로 접근 가능 - 인덱스는 0부터 시작
멤버와 연산자 함수
설명
push_back (element)
벡터의 마지막에 element 추가
at (int index)
index 위치의 원소에 대한 참조 리턴
begin ()
벡터의 첫 번째 원소에 대한 참조 리턴
end ()
벡터의 끝(마지막 원소 다음)을 가리키는 참조 리턴
empty ()
벡터가 비어 있으면 true 리턴
erase (iterator it)
벡터에서 it가 가리키는 원소 삭제, 삭제 후 자동으로 벡터 조절
insert (iterator it, element)
벡터 내 it 위치에 element 삽입
size()
벡터에 들어있는 원소의 개수 리턴
operator[]()
지정된 원소에 대한 참조 리턴
operator=()
이 벡터를 다른 벡터에 치환(복사)

vector 다루기 사례

// vector 생성 vector<int> v; // 정수 원소 삽입 v.push_back(1); v.push_back(2); v.push_back(3); // 원소 개수 s, 벡터 용량 c int s = v.size(); int c = v.capacity(); // 원소 값 접근 v.at(2) = 5; int n = v.at(1); // 원소 값 접근 v[0] = 10; int m = v[2];
C++
복사
예제 10-9) vector 컨테이너 활용하기
#include <iostream> #include <vector> using namespace std; int main() { vector<int> v; // 정수만 삽입 가능한 벡터 생성 v.push_back(1); v.push_back(2); v.push_back(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로 변경 for (int i = 0; i < v.size(); i++) cout << v[i] << " "; cout << endl; }
C++
복사
예제 10-10) 문자열을 저장하는 벡터 만들기 연습
#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++) { 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; }
C++
복사

iterator

반복자
컨테이너의 원소를 가리키는 포인터
iterator 변수 선언
구체적인 컨테이너를 지정하여 반복자 변수 생성
vector<int>::iterator it; it = v.begin();
C++
복사
→ it는 원소가 int 타입인 벡터의 원소에 대한 포인터
// 벡터 생성 vector<int> v; for (int i = 1; i < 4; i++) v.push_back(i); // iteraotr 변수 선언 및 초기화 vector<int>::iterator it; it = v.begin(); // iterator 증가 it++; // 원소 읽기 int n = *it // 원소 쓰기 n = n*2; *it = n; // 원소 삭제 it = v.erase(it); // 끝으로 옮기기 it = v.end();
C++
복사
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; *it = n; // it가 가리키는 원소에 값 쓰기 } for (it = v.begin(); it != v.end(); it++) // 벡터 v의 모든 원소 출력 cout << *it << ' '; cout << endl; }
C++
복사

map 컨테이너

특징
(’키', ‘값') 의 쌍을 원소로 저장하는 제네릭 컨테이너
동일한 ‘키’를 가진 원소가 중복 저장되면 오류 발생
‘키'로 ‘값' 검색
#include <map>
C++
복사
맵 컨테이너 생성 예
// 맵 생성 Map<string, string> dic; // 원소 저장 dic.insert(mak_pair("love", "사랑")); dic["love"] = "사랑"; // 원소 검색 string kor = dic["love"]; string kor = dic.at("love");
C++
복사
멤버와 연산자 함수
설명
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=()
맵 치환(복사)
예제 10-12) map으로 영한 사전 만들기
#include <iostream> #include <string> #include <map> using namespace std; int main() { map<string, string> dic; dic.insert(make_pair("love", "사랑")); dic.insert(make_pair("apple", "사과")); dic["cherry"] = "체리"; cout << "저장된 단어 개수 " << dic.size() << endl; string eng; while (true) { cout << "찾고 싶은 단어>> "; getline(cin, eng); if (eng == "exit") break; if (dic.find(eng) == dic.end()) // eng "키"를 끝까지 찾았는데 없다면 cout << "None" << endl; else cout << dic[eng] << endl; } cout << "exit" << endl; }
C++
복사

STL 알고리즘 사용하기

알고리즘 함수

템플릿 함수
전역 함수
STL 컨테이너 클래스의 멤버 함수가 아님

sort() 함수 사례

두 개의 매개변수
첫번재 매개변수 : sorting을 시작한 원소의 주소
두번째 매개변수 : sorting 범위의 마지막 원소 다음 주소
예제 10-13) sort() 함수를 이용한 vector sorting
#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); // 키보드에서 읽은 정수를 벡터에 삽입 } sort(v.begin(), v.end()); // v.begin()에서 v.end() 사이의 값을 오름차순으로 정렬 // sort() 함수의 실행 결과 벡터 v의 원소 순서가 변경됨 vector<int>::iterator it; // 벡터 내의 원소를 탐색하는 iterator 변수 선언 for (it = v.begin(); it != v.end(); it++) cout << *it << ' '; cout << endl; }
C++
복사

auto를 이용하여 쉬운 변수 선언

c++ auto

기능
컴파일러에게 변수 선언문에서 추론하여, 타입을 자동 선언하도록 지시
장점
복잡한 변수 선언을 간소하게, 긴 타입 선언 시 오타 줄인다
auto pi = 3.14 auto n = 3; auto *p = &n; int n = 10; int &ref = n; auto ref2 = ref; // ref2는 int& 변수로 자동 선언
C++
복사
함수의 리턴 타입으로부터 추론하여, 변수 타입 선언
int square(int x) { return x * x; } ... auto ret = square(3) // 변수 ret는 int 타입으로 추론
C++
복사
STL 템플릿에 활용
vector<int>::iterator it; // 벡터 내의 원소를 탐색하는 iterator 변수 선언 for (it = v.begin(); it != v.end(); it++) cout << *it << ' ';
C++
복사
이거를
for (auto it = v.begin(); it != v.end(); it++) cout << *it << endl;
C++
복사
이렇게 간단히 선언 가능
예제 10-14) auto 이용한 변수 선언
#include <iostream> #include <vector> using namespace std; int square(int x) { return x * x; } int main() { auto c = 'a'; auto pi = 3.14; auto ten = 10; auto *p = &ten; cout << c << " " << pi << " " << ten << " " << *p << endl; auto ret = square(3); cout << *p << " " << ret << endl; vector<int> v = { 1, 2, 3, 4, 5 }; vector<int>::iterator it; for (it = v.begin(); it != v.end(); it++) cout << *it << " "; cout << endl; for (auto it = v.begin(); it != v.end(); it++) cout << *it << " "; }
C++
복사

람다 (lambda)

람다 대수와 람다식

람다 대수에서 람다식은 수학 함수를 단순하게 표현하는 기법
구성
캡쳐 리스트
매개변수 리스트
리턴 타입
함수 바디
[ ]( ) -> 리턴타입 { /* 함수코드 작성*/ }; [](int x, int y){cout << x+y;}; [](int x, int y)->int{return x + y;}; [](int x, int y){cout << x+y;}(2, 3);
C++
복사
#include <iostream> using namespace std; int main() { [](int x, int y){cout << "합은 " << x+y;}(2, 3); }
C++
복사
예제 10-16) auto로 람다식 다루기
#include <iostream> #include <string> using namespace std; int main() { auto love = [](string a, string b) { cout << a << "보다" << b << "가 좋아" << endl; }; love("돈", "너"); love("냉면", "만두"); }
C++
복사
예제 10-17) 반지름 r이 원의 면적으로 리턴하는 람다식 만들기
#include <iostream> using namespace std; int main() { double pi = 3.14; auto calc = [pi](int r) -> double { return pi*r*r; }; cout << "면적은 " << calc(3); }
C++
복사
예제 10-18) 캡쳐 리스트에 참조 활용, 합을 외부에 저장하는 람다식 만들기
#include <iostream> using namespace std; int main() { int sum = 0; [&sum](int x, int y) { sum = x + y; }(2, 3); // 합 5를 지역변수 sum에 저장 cout << "합은 " << sum; }
C++
복사
예제 10-19) STL for_each() 함수를 이용하여 벡터의 모든 원소 출력
#include <iostream> #include <vector> #include <algorithm> using namespace std; void print(int n) { cout << n << " "; } int main() { vector<int> v = {1, 2, 3, 4, 5}; for_each(v.begin(), v.end(), print); // for_each()는 벡터 v의 첫번째 원소부터 끝까지 검색하면서, // 각 원소에 대해 print(int n) 호출, 매개변수 n에 각 원소 값 전달 }
C++
복사
예제 10-20) STL 함수 for-each() 와 람다식을 이요하여, 벡터의 모든 원소 출력
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> v = { 1, 2, 3, 4, 5 }; for_each(v.begin(), v.end(), [](int n) { cout << n << " "; }); // for_each()는 벡터 v의 첫번째 원소부터 끝까지 검색하면서 // 각 원소에 대해 3번째 매개변수인 람다식 호출, 매개변수 n에 각 원소 값 전달 // 람다식 호출 -> 매개변수 n에는 벡터의 각 원소 전달 }
C++
복사