18. STL 반복자(STL Iterator)
1. 키워드
- STL 반복자(STL Iterator)
- STL(Standard Template Library)
- 반복자(Iterator), 컨테이너(Container), 알고리즘(Algorithm)
- 입력 반복자(Input Iterator)
- 출력 반복자(Output Iterator)
- 순방향 반복자(Forward Iterator)
- 양방향 반복자(Bidirectional Iterator)
- 임의 접근 반복자(Random Access Iterator)
iterator
헤더 파일- 스트림 반복자(Stream Iterator)
- 삽입 반복자(Insert Iterator)
- 역방향 반복자(Reverse Iterator)
- 상수 반복자(Constant Iterator)
2. 반복자
1) STL
- C++이 가지는 프로그래밍 언어로서의 특징 중 하나로 일반화 프로그래밍을 들 수 있다.
- 이러한 일반화 프로그래밍은 데이터를 중시하는 OOP와는 달리 프로그램의 알고리즘에 그 중점을 둔다.
- C++ 표준 템플릿 라이브러리인 STL도 이러한 일반화 프로그래밍 패러다임의 한 축을 담당하고 있다.
- STL은 알고리즘을 일반화한 표현을 제공하며, 데이터의 추상화와 코드를 재활용할 수 있게 한다.
2) STL의 구성 요소
- C++ 표준 템플릿 라이브러리인 STL은 다음과 같은 구성 요소로 이루어진 템플릿을 제공한다.
1] 반복자
2] 컨테이너
3] 알고리즘
3) 컨테이너
- STL에서 컨테이너는 같은 타입의 여러 객체를 저장하는 일종의 집합이라 할 수 있다.
- 컨테이너는 클래스 템플릿으로, 컨테이너 변수를 선언할 때 컨테이너에 포함할 요소의 타입을 명시할 수 있다.
4) 반복자
- 반복자란 STL 컨테이너에 저장된 요소를 반복적으로 순회하여, 각각의 요소에 대한 접근을 제공하는 객체이다.
- 즉, 컨테이너의 구조나 요소의 타입과는 상관없이 컨테이너에 저장된 데이터를 순회하는 과정을 일반화한 표현이다.
- 템플릿이 타입과 상관없이 알고리즘을 표현할 수 있게 해준다면, 반복자는 컨테이너와 상관없이 알고리즘을 표현할 수 있게 해주는 것이다.
- 반복자가 가져야 할 요구 사항과 정의되어야 할 연산자는 다음과 같다.
1] 가리키는 요소의 값에 접근할 수 있어야 하기 때문에 *
(참조 연산자)가 정의되어야 한다.
2] 반복자 사이의 대입 연산, 비교 연산이 가능해야 하기 때문에 =
(대입 연산자) 및 관계 연산자가 정의되어야 한다.
3] 가리키는 요소의 주변 요소로 이동할 수 있어야 하기 때문에 ++
(증가 연산자)가 정의되어야 한다.
- 위와 같은 요구 사항을 모두 갖춰야만 STL 알고리즘에서 반복자로 사용될 수 있다.
- 참고로 반복자는 포인터를 일반화한 것으로, 포인터는 반복자가 가져야 할 모든 요구 사항을 만족한다.
3. 반복자의 종류
1) 반복자의 종류
- STL은 제공하는 기능에 따라 반복자를 다음과 같이 다섯 가지로 분류하고 있다.
1] 입력 반복자
2] 출력 반복자
3] 순방향 반복자
4] 양방향 반복자
5] 임의 접근 반복자
(1) 입력 반복자
- 입력 반복자는 가장 단순한 형태의 반복자로, 컨테이너로부터 값을 읽는 데 사용된다.
- 입력 반복자를 사용하면 컨테이너로부터 값을 읽을 수는 있지만, 프로그램이 그 값을 변경할 수는 없다.
- 따라서 이 반복자는 읽기 전용 알고리즘에서 사용할 수 있다.
- 입력 반복자는
++
를 사용하여 순방향으로만 이동할 수 있다. - 또한,
*
를 사용하여 반복해서 요소를 참조할 수 있다.
- 다음의 코드는 순차 검색을 통해 컨테이너에 포함된 특정 값을 찾아내는
Find()
함수의 원형을 보여준다.
template <class InputIterator, class T>
InputIterator Find(InputIterator first, InputIterator last, const T &value);
(2) 출력 반복자
- 출력 반복자는 입력 반복자와는 반대로 컨테이너의 값을 변경하는 데 사용된다.
- 출력 반복자를 사용하면 컨테이너의 값을 변경할 수는 있지만, 프로그램에서 값을 읽을 수는 없다.
- 따라서 이 반복자는 쓰기 전용 알고리즘에 사용할 수 있다.
- 출력 반복자는
++
를 사용하여 순방향으로만 이동할 수 있다. - 또한,
*
를 사용하여 단 한 번만 요소에 값을 쓸 수 있다.
- 다음의 코드는 한 컨테이너에서 다른 컨테이너로 값을 복사하는
Copy()
함수의 원형을 보여준다.
template<class InputIterator, class OutputIterator>
OutputIterator Copy(InputIterator first, InputIterator last, OutputIterator result);
- 참고로 입력 반복자와 출력 반복자는 입출력 스트림에만 적용된다.
(3) 순방향 반복자
- 순방향 반복자는 입출력이 모두 가능한 반복자이다.
- 따라서 이 반복자는 다중 패스 알고리즘에 사용할 수 있다.
- 순방향 반복자는
++
를 사용하여 순방향으로만 이동할 수 있다. - 또한,
*
를 사용하여 몇 번이고 반복해서 같은 요소를 참조하거나, 그 값을 변경할 수 있다. - 즉, 순방향 반복자는 입력 반복자와 출력 반복자의 기능을 모두 포함하고 있다.
- 다음의 코드는 특정 값을 찾아 다른 값으로 변경하는
Replace()
함수의 원형을 보여준다.
template<class ForwardIterator, class T>
void Replace(ForwardIterator first, ForwardIterator last, const T &target, const T &replacement);
(4) 양방향 반복자
- 양방향 반복자는 입출력이 모두 가능한 반복자이다.
- 양방향 반복자는
++
를 사용하면 순방향으로,--
를 사용하면 역방향으로도 이동할 수 있다. - 또한,
*
를 사용하여 몇 번이고 반복해서 같은 요소를 참조하거나, 그 값을 변경할 수 있다. - 즉, 양방향 반복자는 순방향 반복자의 기능을 모두 포함하고 있다.
- 다음의 코드는 특정 컨테이너의 모든 값을 거꾸로 새로운 컨테이너에 옮기는
Reverse()
함수의 원형을 보여준다.
template<class BidirectionalIterator, class OutputIterator>
OutputIterator Reverse(BidirectionalIterator first, BidirectionalIterator last, OutputIterator result);
(5) 임의 접근 반복자
- 임의 접근 반복자는 최상위 레벨의 반복자로서 가장 많은 기능을 제공한다.
- 임의 접근 반복자는 양방향 반복자의 모든 기능을 포함하고 있으며,
[]
(첨자 연산자)를 통해 임의의 요소에 접근할 수 있다. - 또한, 증감 연산자를 통해 양방향으로 이동할 수 있으며, 요소의 순서를 결정하기 위해 관계 연산자를 사용할 수 있다.
- 즉, 임의 접근 반복자는 양방향 반복자의 모든 기능과 함께 기존의 일반 포인터가 하는 거의 모든 일을 할 수 있다.
- 다음의 코드는 특정 컨테이너의 모든 값을 정렬시키는
Sort()
함수의 원형을 보여준다.
template<class RandomAccessIterator>
void Sort(RandomAccessIterator first, RandomAccessIterator last);
2) 반복자 계층
- STL의 반복자는 계층적으로 분류된다.
- 예를 들어, 순방향 반복자는 입력 반복자와 출력 반복자의 모든 기능을 포함하고 거기에 자신만의 기능을 추가한다.
- 이렇게 다양한 반복자를 사용하는 이유는 알고리즘의 적용 조건을 제한하기 위해서이다.
- 즉, 양방향 반복자는 임의 접근 반복자에 기초하는 알고리즘은 사용할 수 없지만, 자신보다 하위 계층의 반복자에 기초하는 알고리즘은 사용할 수 있는 것이다.
- 다음 표는 STL 주요 반복자의 기능을 계층 순으로 정리한 것이다.
반복자의 기능 | 입력 | 출력 | 순방향 | 양방향 | 임의 접근 |
---|---|---|---|---|---|
접근(-> ) |
O | X | O | O | O |
읽기(* ) |
O | X | O | O | O |
쓰기(* ) |
X | O | O | O | O |
증가 연산자(++ ) |
O | O | O | O | O |
감소 연산자(-- ) |
X | X | X | O | O |
첨자 연산자([] ) |
X | X | X | X | O |
산술 연산자(+ , - ) |
X | X | X | X | O |
산술 대입 연산자(+= , -= ) |
X | X | X | X | O |
관계 연산자(< , <= , > , >= ) |
X | X | X | X | O |
4. 기타 반복자
1) iterator
헤더 파일
- STL은
iterator
헤더 파일을 통해 미리 정의된 몇 가지 반복자를 제공한다. - 이 헤더 파일은 미리 정의된 반복자뿐만 아니라 스트림 반복자, 반복자의 원형 그리고 여러 지원 템플릿을 포함하고 있다.
2) 스트림 반복자
- 스트림 반복자는 반복자 연산을 통해 알고리즘이 입출력 스트림에 보다 쉽게 접근할 수 있게 해준다.
- 이때 STL은 입력과 출력 스트림을 각각 입력과 출력 반복자로 변환하는 방식으로 제공한다.
- 입력 스트림 반복자는
istream_iterator
, 출력 스트림 반복자는ostream_iterator
클래스 템플릿에서 제공한다.
istream_iterator
클래스 템플릿은 다음과 같이 정의된다.
template <class T, class charT = char, class traits = char_traits<charT>, class Distance = ptrdiff_t>
class istream_iterator : public iterator<input_iterator_tag, T, Distance, const T *, const T &> { ... }
ostream_iterator
클래스 템플릿은 다음과 같이 정의된다.
template <class T, class charT = char, class traits = char_traits<charT> >
class ostream_iterator : public iterator<output_iterator_tag, void, void, void, void> { ... }
istream_iterator
와ostream_iterator
클래스 템플릿은 첫 번째를 제외한 인수에 모두 디폴트 인수를 명시하고 있다.- 따라서 사용자는 입출력하고자 하는 데이터의 타입만을 인수로 전달하여 간편하게 사용할 수 있다.
- 다음의 코드는
copy()
함수를 사용하여vector
객체의 첫 요소부터 마지막 요소까지를 모두 출력하는 것이다.
#include <iostream>
#include <iterator>
#include <vector>
using namespace std;
int main(void) {
vector<int> vc = {1, 2, 3, 4, 5};
copy(vc.begin(), vc.end(), ostream_iterator<int>(cout));
cout << endl;
copy(vc.begin(), vc.end(), ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}
// 12345
// 1 2 3 4 5
- 위의 코드에서는 C++ 표준 출력 객체인
cout
객체를ostream_iterator
클래스 템플릿의 인수로 전달하여 모니터에 대한 출력을 수행한다.
3) 삽입 반복자
- 알고리즘에서 출력 반복자를 사용하면 반복자가 가리키는 요소의 값을 덮어쓰게 된다.
- 하지만 삽입 반복자는 값을 덮어쓰지 않고, 그 요소의 위치에 새로운 값을 삽입할 수 있게 해준다.
- 즉, 삽입 반복자는 반복자의 복사 연산을 삽입 연산으로 변환해 주는 역할을 한다.
- STL에서 제공하는 삽입 반복자는 다음과 같다.
1] insert_iterator
2] back_insert_iterator
3] front_insert_iterator
삽입 반복자 | 설명 | 사용 멤버 함수 | 사용가능 컨테이너 |
---|---|---|---|
insert_iterator |
해당 컨테이너의 특정 위치에 삽입함 | insert() |
모든 컨테이너 |
back_insert_iterator |
해당 컨테이너의 뒤쪽에 삽입함 | push_back() |
시퀀스 컨테이너 ( vector , list , deque ) |
front_insert_iterator |
해당 컨테이너의 앞쪽에 삽입함 | push_front() |
list , deque |
- 다음의 코드는
push_back()
함수와push_front()
함수의 삽입 반복자를 사용하여 리스트에 요소를 삽입하는 것이다.
#include <iostream>
#include <iterator>
#include <list>
using namespace std;
int main(void) {
list<int> ls = {10};
ls.push_back(20); // back_insert_iterator를 사용함
ls.push_front(30); // front_insert_iterator를 사용함
copy(ls.begin(), ls.end(), ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}
// 30 10 20
4) 역방향 반복자
- 역방향 반복자는 순방향 반복자와는 반대 방향으로 동작하는 반복자이다.
- 즉, 역방향 반복자의
++
는 순방향 반복자의 역방향으로 이동하게 된다. rbegin()
과rend()
멤버 함수를 사용하면 자동으로reverse_iterator
를 반환한다.
- 다음의 코드는
rbegin()
과rend()
함수의 역방향 반복자를 사용하여 리스트의 요소를 역순으로 출력하는 것이다.
#include <iostream>
#include <iterator>
#include <list>
using namespace std;
int main(void) {
list<int> ls = {10, 20, 30};
copy(ls.rbegin(), ls.rend(), ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}
// 30 20 10
5) 상수 반복자
- 앞서 살펴본 것처럼 반복자를 동작 방식으로 분류하는 것 외에도 반복자가 가리키는 값의 변경이 가능한가로 분류할 수도 있다.
- 이때 반복자가 가리키는 값의 변경이 불가능한 반복자를 상수 반복자라고 한다.
- 단, 상수 반복자란 가리키는 요소를 상수화시킨 것이지 반복자 그 자체를 상수화시킨 것은 아니다.
- 따라서 양방향 상수 반복자라면 앞뒤로 이동하며 다른 요소를 가리키는 것이 가능하다.
- 이러한 상수 반복자의 타입은
const_iterator
타입으로 정의된다.
- 다음의 코드는 반복자를 사용하여
list
의 첫 번째 요소의 값을 변경하는 것이다.
#include <iostream>
#include <iterator>
#include <list>
using namespace std;
int main(void) {
list<int> ls = {10, 20, 30};
list<int>::iterator iter;
list<int>::const_iterator citer;
iter = ls.begin();
*iter = 100;
citer = ls.end();
// *citer = 300; // 상수 반복자이므로 값의 변경은 불가능함
for (citer = ls.begin(); citer != ls.end(); citer++) {
cout << *citer << " ";
}
cout << endl;
return 0;
}
// 100 20 30
- 위의 코드에서 상수 반복자로 선언된
citer
는 가리키는 값의 변경이 불가능하며, 값을 읽는 작업에만 사용할 수 있다.