▣ 표준 템플릿 라이브러리(STL)
● Standard Template Library, STL
▷ STL이란?
- C++이 제공하는 표준 컨테이너 클래스 템플릿 라이브러리
- vector, list, stack, queue 등의 Container와 이들을 처리하기 위한 여러 연산을 포함하고 있다.
▷ STL의 구성 요소
- 컨테이너 container : 데이터 저장 기능
- 반복자 iterator : 포인터 역할
- 알고리즘 algorithm : 데이터 처리 기능
● STL container 컨테이너
▷ 데이터 저장을 위한 template의 집합이다.
- int나 float과 같은 기본 자료형 데이터나 사용자 정의 클래스의 객체 등을 저장한다.
- 다양한 연산이 제공되어서 편리하게 데이터를 활용할 수 있다.
- ++) 배열이 일종의 컨테이너의 역할을 하지만 배열의 경우에는 조작하기 위한 연산을 프로그래머가 모두 구현해야 한다. 여기서 연산에는 특정 위치에 데이터를 삽입, 데이터를 삭제, 데이터를 검색 하는 연산을 포함한다.
● STL container 종류
▷ 순차 컨테이너 sequential container
- 동일한 자료형의 객체들을 선형적인 구조로 저장한다.
종류 | 특성 |
vector | - 크기의 확장이 가능한 배열이라고 볼 수 있다. - [ ] 연산자로 지정한 첨자를 이용하여 빠른 직접 접근한다. - 크기 확장이 가능해서 끝에 삽입/삭제 하는 것은 빠르지만 그 외의 위치에 삽입/삭제 하는 것은 느리다. (삽입/삭제하려는 위치 위에 있는 데이터를 옮겨야 해서) |
list | - 이중 연결 리스트 double linked list 형태의 구조다. - 어느 위치에는 삽입/삭제가 효율적이다. - 직접 접근이 비효율적이라 제공하지 않는다. (왜냐하면 처음 위치부터 원하는 위치까지 순차적으로 데이터를 봐야하기 때문이다.) |
deque | - vector와 list의 혼합 형태다. - vector와 list의 특성이 모두 필요할 때 사용할 수 있지만 성능은 낮다. |
▷ 연상 컨테이너 associative container
- 탐색 트리와 같은 인덱스 구조를 이용하는 컨테이너다.
- 키를 이용한 효율적인 검색 기능을 제공한다.
종류 | 특성 |
set | - (key 객체)만 저장하고, key가 중복되지 않는다. |
multiset | - (key 객체)만 저장하고, 동일한 key가 중복될 수도 있다. |
map | - (key 객체, value 객체)의 쌍을 저장한다. - key가 중복되지 않는다. |
multimap | - (key 객체, value 객체)의 쌍을 저장한다. - 동일한 key가 중복될 수 있다. |
▷ 무순서 연상 컨테이너 unordered associative container
- 연상 컨테이너처럼 key를 이용한 검색기능을 제공한다.
- 해시함수를 이용해서 데이터 검색 시간이 일정하다.
종류 | 특성 |
unordered_set | - set, multiset, map, multimap과 같지만 해시함수를 이용해서 저장과 검색을 한다는 것에서 다르다. |
unordered_multiset | |
unordered_map | |
unordered_multimap |
인덱스를 이용해서 데이터를 찾는 방식은 데이터의 크기가 커지면 인덱스도 많아져야 하기 때문에 비효율적일 수 있다. 해시함수를 이용하면 데이터의 양에 관계 없이 데이터 검색 시간이 일정해진다. key의 순서대로 데이터를 검색하는 것 같은 일은 하기 어렵다. 그래서 무순서 연상 컨테이너라고 한다.
▷ 컨테이너 어뎁터 container adaptor
- 기본 컨테이너를 기반으로 특정 용도에 맞게 고안된 컨테이너다.
종류 | 특성 |
queue | - FIFO(First In, First Out) 구조 |
priority_queue | - 우선순위에 따라 데이터를 액세스할 수 있는 구조 |
stack | - LIFO(Last In, First Out) 구조 |
● 반복자 iterator
▷ iterator가 뭔가?
- 컨테이너 안의 데이터를 가리키는 포인터의 역할을 한다.
- 포인터의 개념이 일반화된 것이다.
- 컨테이너의 유형에 따라 서로 다른 형태의 iterator가 사용된다.
종류 | 특성 |
순방향(forward) iterator | - 컨테이너의 순방향으로만 움직일 수 있다. - (++) 연산자를 사용한다. |
양방향(bidirectional) iterator | - 컨테이너의 순방향과 역방향으로 움직일 수 있다. - (++), (--) 연산자를 사용한다. |
Random Access iterator | - 양방향 iterator의 기능과 함께 임의의 위치로 이동할 수 있다. |
● 알고리즘 Algorithm
▷ 컨테이너의 원소에 대해 적용 가능한 여러 연산
알고리즘 | 용도 |
search | - 지정된 값과 동일한 첫 번째 원소 반환 |
count | - 지정된 값을 갖는 원소의 수 반환 |
swap | - container 내의 값을 교환 |
sort | - container의 값들을 지정된 순서에 따라 정렬 |
merge | - 두 정렬된 영역의 원소들을 합병 |
reverse | - 지정된 범위의 원소들을 역순으로 나열 |
remove | - container에서 지정된 값을 제거 |
replace | - 지정된 값을 다른 값으로 대체 |
unique | - 인접 위치에 있는 중복된 값을 제거 |
for_each | - 지정된 함수를 container의 모든 원소에 적용 |
▣ STL 활용 예 - vector
● vector
▷ vector가 뭔가?
- 1차원 배열의 개념을 구현한 순차 컨테이너 유형의 class template
- 배열의 일반적인 기능을 가지면서 여러 유용한 멤버함수 및 관리 기능도 갖고 있다.
- 배열처럼 크기가 고정돼있지 않고 필요에 따라 저장 공간 확장 가능하다.
- #include 명령으로 헤더파일 <vector>를 프로그램에 삽입하면 된다.
▷ vector 객체 선언 구문
vector<ClassName> objName(n);
// n: 벡터에 저장할 객체의 수
- vector 객체 선언 예시 - 10개의 int 값을 저장하는 vector 선언
vector<int> iVector(10);
▷ [ ] 연산자
- vector에 대한 직접접근 연산자로 배열처럼 첨자를 지정해서 원소를 직접접근하게 한다.
- [ ] 연산자 사용 예시
vector<int> iVector(10);
iVector[2] = 5;
cout << iVector[2];
▷ 멤버함수 size( )와 capacity( )
- vector의 크기는 실행 중에도 확장할 수 있다.
- 미래의 확장에 대비하여 여분의 공간을 미리 확보할 수 있다.
- 논리적인 vector 크기와 실제 할당된 메모리 크기가 다를 수 있다.
- vector의 확장을 위해 size밖의 capacity 공간까지 확장할 수 있다.
▷ 멤버함수 push_back( )과 pop_back( )
- push_back( ) : vector의 끝에 데이터를 저장
- pop_back( ) : vector의 끝의 데이터를 꺼낸다.
- 이 동작들에 의해 vector의 논리적 크기(size)가 바뀐다. (물론 컴파일러마다 그 정도가 다름)
▷ 멤버함수 insert( )과 erase( )
- insert( ) : vector의 지정된 위치에 데이터를 삽입한다.
- erase( ) : vector의 지정된 위치의 데이터를 삭제한다.
- 함수의 실행에 따라 논리적인 데이터의 크기인 size( )의 값이 증가/감소 한다.
- capacity( )의 값은 데이터 추가로 인해 확보된 메모리가 부족해서 확장할 때 바뀐다.
● vector 사용의 예시
▷Vector1.cpp
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> intVec(5);
for (int i = 0; i < intVec.size(); i++)
intVec[i] = i + 1;
cout << "vector의 논리적 크기 : " << intVec.size() << endl;
cout << "vector의 물리적 크기 : " << intVec.capacity() << endl;
cout << "vector에 저장된 데이터 : ";
for (int i = 0; i < intVec.size(); i++)
cout << intVec[i] << " ";
cout << endl << endl << "1개 데이터를 push_back" << endl;
intVec.push_back(11); // intVec[5] = 11; size +1 up
cout << "vector의 논리적 크기 : " << intVec.size() << endl;
cout << "vector의 물리적 크기 : " << intVec.capacity() << endl;
cout << "vector에 저장된 데이터 : ";
for (int i = 0; i < intVec.size(); i++)
cout << intVec[i] << " ";
cout << endl << endl << "5개 데이터를 push_back" << endl;
for (int i = 1; i <= 5; i++)
intVec.push_back(i + 11);
cout << "vector의 논리적 크기 : " << intVec.size() << endl;
cout << "vector의 물리적 크기 : " << intVec.capacity() << endl;
cout << "vector에 저장된 데이터 : ";
for (int i = 0; i < intVec.size(); i++)
cout << intVec[i] << " ";
cout << endl << endl << "3개 데이터를 pop_back" << endl;
for (int i = 0; i < 3; i++)
intVec.pop_back();
cout << "vector의 논리적 크기 : " << intVec.size() << endl;
cout << "vector의 물리적 크기 : " << intVec.capacity() << endl;
cout << "vector에 저장된 데이터 : ";
for (int i = 0; i < intVec.size(); i++)
cout << intVec[i] << " ";
cout << endl;
return 0;
}
▷ Vector1.cpp 출력결과
- C++14로 컴파일러를 이용한 경우에 물리적 크기인 capacity가 논리적 크기에 비해 어느정도 더 여유있게 배당되었는지 볼 수 있다. (컴파일러마다 다르다)
- 물리적 크기를 보고 얼마나 더 vector를 확장할 수 있는지에 주목하라.
- 데이터 삽입, 삭제의 구현이 가능하다.
● vector의 크기 확장 및 데이터 조작 함수
조작 함수 | 용도 |
push_back(value) | - 끝에 데이터 추가 |
pop_back( ) | - 끝에 있는 데이터 제거 |
resize(n) | - 논리적 크기 변경 |
reserve(n) | - capacity( )가 최소한 n을 반환하도록 확장 |
empty( ) | - 비어 있는 벡터의 경우 true 반환 |
erase(it) | - 반복자 it가 가리키는 위치 삭제 |
erase(it1, it2) | - [it1, it2) 범위의 데이터 삭제 // it1 <= i < it2 |
insert(it, value) | - 반복자 it가 가리키는 위치에 value 삽입 |
● vector와 반복자
▷반복자 선언
vector<ClassName>::iterator it;
▷반복자의 값을 구하는 vector의 멤버함수
- begin( ) : 첫 번째 원소를 가리키는 random access 반복자를 반환 (포인터 위치)
- end( ) : 마지막 원소의 다음 위치를 가리키는 random access 반복자를 반환
● vector의 반복자 활용 예시
▷ Vector2.cpp
- random access 반복자 이용해서 지정된 위치의 데이터 읽기가 가능하다.
- 반복자를 이용해서 vector에 저장된 데이터에 포인터와 같이 접근할 수 있고, 직접접근이 가능하다.
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> intVec(5);
for (int i = 0; i < intVec.size(); i++)
intVec[i] = i + 1;
// 1번 방법
//vector<int>::iterator it = intVec.begin();
// 2번 방법
auto it = intVec.begin(); // 자료형 추론(auto)을 이용한 반복자 선언
cout << "vector에 저장된 데이터 : ";
for (; it < intVec.end(); it++) //초기화 생략
cout << *it << " ";
cout << endl;
it = intVec.begin();
cout << "3번째 데이터 : "; //random access 반복자 가능
cout << *(it + 2) << endl;
return 0;
}
▷ Vector2.cpp 출력결과
▣ 알고리즘의 활용
● sort( ) 함수
▷ sort( )의 용법
- random access 반복자로 지정된 범위의 값들을 순서대로 정렬한다.
//(1)번 유형
sort(first, last);
//(2)번 유형
sort(first, last, comp);
//first: 정렬할 범위의 시작 원소에 대한 포인터
//last: 정렬할 범위의 마지막 원소의 다음 위치에 대한 포인터
//comp: 정렬 순서를 정하는 함수 (callback 함수)
// a의 순서가 b보다 앞인 경우 comp(a,b) == true
- first: 정렬할 범위의 시작 원소에 대한 포인터
- last: 정렬할 범위의 마지막 원소의 다음 위치에 대한 포인터
- comp: 정렬 순서를 정하는 함수 (callback 함수)
- a의 순서가 b보다 앞인 경우 comp(a,b) == true
- 나중에 사용할 수 있게끔 포인터를 전달하는 함수 - callback 함수
● merge( ) 함수
▷ merge( )의 용법
- 동일한 기준으로 정렬된 두 개의 데이터 집합을 동일한 기준으로 정렬된 하나의 데이터 집합으로 결합하는 함수다.
// (1) 유형
merge(first1, last1, first2, last2, dest);
// (2) 유형
merge(first1, last1, first2, last2, dest, comp);
// first1, last1 : 첫 번째 정렬된 데이터 범위
// first2, last2 : 두 번째 정렬된 데이터 범위
// dest : 합병 결과가 저장될 시작 위치
// comp : 합병 순서를 정하는 함수
- first1, last1 : 첫 번째 정렬된 데이터 범위
- first2, last2 : 두 번째 정렬된 데이터 범위
- dest : 합병 결과가 저장될 시작 위치
- comp : 합병 순서를 정하는 함수
● 알고리즘의 활용 예시
▷ Vector3.cpp
- 난수 생성을 하기 위해 srand( ) 함수를 이용하는데, 이 함수를 이용하기 위해서는 <cstdlib>를 #include 해야 한다.
- srand( )에는 인수로 초깃값을 줘야 하는데, 그 초깃값을 현재 시간에 기반해서 랜덤하게 주기 위해 time( ) 함수를 사용하고 이를 위해 <ctime>라이브러리를 포함한다.
- iv1의 데이터를 바꾸기 위해 auto &i를 통해 반복자를 참조로 받는다.
- sort 함수로 iv1.begin부터 iv1.end까지 정렬하라 했을 때 iv1.end는 데이터가 없는 공간을 가리키므로 그 전 데이터가 있는 벡터까지만 정렬할 것이다.
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <algorithm>
using namespace std;
int main()
{
srand((unsigned)time(NULL)); // random number
// vector 1
vector<int> iv1(5);
cout << "vector1 : ";
for (auto &i : iv1){ //auto로 i의 자료형을 정수형으로 추론할 것이다.
i = rand() % 100; // 0 ~ 99
cout << i << " ";
}
sort(iv1.begin(), iv1.end()); // 정렬 알고리즘
cout << endl << "정렬된 vector1 : ";
for (auto i : iv1)
cout << i << " ";
cout << endl << endl;
vector<int> iv2(5);
cout << "vector2 : ";
for (auto &i : iv2){
i = rand() % 100; // 0 ~ 99
cout << i << " ";
}
//vector 2
sort(iv2.begin(), iv2.end());
cout << endl << "정렬된 vector2 : ";
for (auto i : iv2)
cout << i << " ";
cout << endl << endl;
//vector 3 <- 합병 결과 저장할 벡터
vector<int> iv3(iv1.size() + iv2.size());
// vector1 + vector2 = vector 3
merge(iv1.begin(), iv1.end(), iv2.begin(), iv2.end(), iv3.begin());
cout << "vecter1 + vector 2 merge 결과 : ";
for (auto i : iv3)
cout << i << " ";
cout << endl << endl;
return 0;
}
▷ Vector3.cpp 출력결과
● 정렬 순서 결정
▷ 정렬 순서 지정을 위한 callback 함수 전달
- sort(first, last, comp);
template<typename T>
bool greaterth(const T& a, const T& b) {
return a > b;
}
void f(vector<int>& iv) {
// 내림차순 정렬
sort(iv.begin(), iv.end(), gt<int>);
}
데이터 양이 많을 땐 gt<int>로 함수가 반복적으로 호출되기 때문에 성능을 많이 저하시킬 수 있으므로 함수 객체를 사용한다.
▷ 함수객체를 이용한 callback 함수 전달
- 함수객체 : 함수처럼 사용될 수 있는 객체
- 함수 호출 연산자 (bool operator() ~ )를 정의하면 함수객체로 사용할 수 있다.
- 정렬함수의 인수로 함수 대신 함수객체를 전달하면 inline 함수를 인수로 넘겨준 것의 효과를 볼 수 있다.
/* 함수객체 정의 */
template<typename T> class GREATER {
public:
bool operator()(const T& a, const T& b) const {
return a > b;
}
};
/* 예시 1 */
GREATER<int> greaterthan;
if (greaterthan(6, 5))
cout << "6이 5보다 크다." << endl;
▼
void f(vector<int>& iv) {
// 내림차순 정렬
sort(iv.begin(), iv.end(), GREATER<int>()); // default 생성자 동작
}
▣ STL 활용 예 - map
● map
▷ map의 정의
- 저장하는 데이터의 형태 : (key, value)의 쌍
- key를 이용해서 데이터에 직접접근할 수 있는 연상 컨테이너
- key : 데이터 집합에서 특정 데이터를 검색하거나 데이터 집합을 정렬할 때 처리의 기준이 되는 속성이다.
- (이름, 전화번호) 에서 key를 이름으로 검색하면 전화번호를 찾을 수 있다. - map에 저장되는 데이터는 key가 모두 다르다.
- 트리 형태의 데이터 구조를 이용해서 검색 시간이 데이터 수의 log 함수에 비레한다.
- 헤더파일 <map>을 사용한다.
● map 이용
▷ map 객체 선언
map<KeyType, ValueType, Traits> objName;
// KeyType : key 자료형
// ValueType : key와 연관된 value 데이터 자료형
// Traits : map 내에서의 상대적 순서를 결정하는 함수객체의 클래스로
// default는 less<KeyType>
- key가 이름, value가 주소, 각각 string 타입일 때, (이름, 주소) 쌍 저장하는 map 객체는 아래와 같이 선언한다.
map<string, string> addrbook;
▷데이터 저장
- insert( ) 함수 : 데이터 삽입 기능
map<string, string> addrbook;
addrbook.insert(make_pair("김남준", "고양시"));
addrbook.insert({"김석진", "과천시"});
addrbook.insert({"민윤기", "대구광역시"});
// pair : first와 second라는 2개의 데이터 멤버를 포함하는 template 구조체
// make_pair : pair 객체를 반환하는 함수 template
// 동일한 key를 갖는 데이터가 이미 존재할 경우 삽입이 이루어지지 않는다.
- [ ] 연산자 : key를 이용한 데이터 직접접근
addrbook (초기 상태)
김남준 | 고양시 |
김석진 | 과천시 |
민윤기 | 대구광역시 |
cout << addrbook["김석진"]; // "과천시" 출력
addrbook["정호석"] = "광주광역시"; // ("정호석", "광주광역시") 삽입
addrbook["김남준"] = "서울특별시"; // "김남준"의 데이터가 수정된다.
addrbook (수정 후 상태)
김남준 | 서울특별시 |
김석진 | 과천시 |
민윤기 | 대구광역시 |
정호석 | 광주광역시 |
- find( ) 함수를 이용한 검색 - 지정된 key를 가지는 데이터를 가리키는 반복자 반환
auto it = addrbook.find("정호석");
// ㄴ-> it는 pair 객체 ("정호석", "광주광역시")를 가리킨다.
auto it = addrbook.find("박지민");
// ㄴ-> it에 addrbook.end()가 저장된다. 왜냐면 박지민이라는 key를 가진 데이터가 없기 때문에
// 마지막 데이터의 그 다음 위치를 가리킨다.
- erase( ) 함수를 이용한 데이터 삭제
1) 삭제할 데이터를 가리키는 반복자를 지정하는 방법
addrbook.erase(it1); //it가 가리키는 데이터삭제
2) 삭제할 데이터를 가리키는 반복자의 범위를 지정하는 방법
addrbook.erase(it1, it2); // [it1, it2) 범위의 데이터 삭제
3) key를 지정하여 데이터를 삭제하는 방법
addrbook.erase("민윤기"); // key가 "민윤기"인 데이터 삭제
● map의 활용 예 - PBookMap.cpp
#include <iostream>
#include <string>
#include <map>
using namespace std;
template<typename T>
class LESS_T {
public:
bool operator()(const T& a, const T& b) const {
return a < b;
}
};
int main()
{
map<string, string, LESS_T<string>> pBook {
{ "Kimnamjoon", "1994-09-12"},
{ "Kimseokjin", "1992-12-04"}
};
pBook["MinYoongi"] = "1993-03-09";
pBook.insert(make_pair("Junghoseok", "1994-02-18"));
pBook.insert({"KimTaehyung", "1995-12-30"});
for (auto pb = pBook.begin(); pb != pBook.end(); ++pb)
cout << pb->first << " " << pb->second << endl;
cout << pBook.size() << "명이 등록되어 있습니다.";
string str;
cout << endl << "찾을 이름: ";
cin >> str;
auto result = pBook.find(str);
if (result != pBook.end()) {
cout << result->first << "생년월일은 ";
cout << result->second << " 입니다." << endl;
}
else
cout << str << "씨를 찾을 수가 없습니다." << endl;
return 0;
}
▷PBookMap.cpp 출력결과
▣ 추가사항
● decltype을 이용한 자료형 추론
▷ decltype(e)
- 컴파일러가 e의 자료형을 추론하게 한다.
- e의 값을 실제로 계산하는 건 아니다.
▷ decltype(e)의 자료형 결정 규칙
- e가 명칭이거나 클래스 멤버에 대한 액세스인 경우 decltype(e)는 e의 자료형이다.
- e가 x-value인 경우 decltype(e)는 e의 자료형에 대한 r-value 참조다.
(x-value : 함수의 r-value 참조를 return해주는 return 타입) - e가 l-value인 경우 decltype(e)는 e의 자료형에 대한 l-value 참조다.
- 그 외의 경우 decltype(e)는 e의 자료형이다.
▷ decltype(e)의 자료형 추론 예시
int x = 10;
double y = 3.14;
const int&& f();
struct A { double val; };
const A* p;
decltype(x) a = x; // a는 int형
decltype(x + y) b; // b는 double형
decltype(++x) c = a; // c는 int형
decltype(int{} + double{}) d; // d는 double형
decltype(f()) f = 20; // f는 const int&&형
decltype(p->val) g; // g는 double형
decltype((p->val)) h = y; // h는 const double&형, 괄호 안에 넣어서 수식을 가리킴
▷ decltype의 활용 예시 - 함수선언
- decltype을 이용해서 함수의 자료형을 선언해주는 경우, template 이용하면 이 기능이 필요해짐 ㅎ
// 1번
double add(int a, double h) {
return a + b;
}
// 2번
decltype(int{} + double{}) add(int a, double b){
return a + b;
}
// 3번 - 오류
decltype(a + b) add(int a, double b){
return a + b;
}
// 4번 - 후행반환형 trailing return type, 리턴타입을 뒤쪽에 쓰는것 (C++!1부터 가능)
auto add(int a + double b) -> decltype(int a, double b){
return a + b;
}
▷ 여러 자료형이 혼합된 템플릿에서의 decltype 활용
- add(const Vec<T1>& v1, const Vec<T2>& v2) 에서 아직 자료형이 정해지지 않은 상태라 어떤 형태로 return할 지 template을 정의할 때는 정해지지 않았다. -> decltype으로 자료형 추론한다.
- 이 때 가독성을 위해 auto를 활용해서 후행반환형으로 써서 가독성을 높여준다.
template <typename T> class Vec {
int n;
T* arr;
public:
Vec(int d, const T* a = nullptr);
.. // size(), [] operator 멤버함수 정의
};
template <typename T1, typename T2>
Vec<decltype(T1{}+T2{})> add(const Vec<T1>& v1, const Vec<T2>& v2)
auto add(const Vec<T1>& v1, const Vec<T2>& v2) -> Vec<decltype(T1{}+T2{})>
{
Vec<decltype(T1{}+T2{})> tVec(v1.size());
for (int i = 0; i < v1.size(); i++) // 벡터 덧셈 연산
tVec.arr[i] = v1[i] + v2[i];
return tVec;
}
'Programming > C++' 카테고리의 다른 글
[Modern C++ 공부 - Day0] C++11, 14, 17는 뭐가 다를까? (1) | 2023.06.15 |
---|---|
C++ 언어 기초 (16) - 예외처리 exception (0) | 2020.09.07 |
C++ 언어 기초 (14) - 템플릿 template (Feat. 버블정렬) (0) | 2020.09.05 |
C++ 언어 기초 (13) - 추상클래스, 다중상속 (0) | 2020.09.04 |
C++ 언어 기초 (11) - 상속; 기초/파생클래스, 접근 제어, final, name binding (0) | 2020.09.03 |