반복자
컨테이너는 데이터를 저장하는 역할을 하지만, 컨테이너에 저장된 데이터에 접근하고 조작하기 위해서는 특별한 도구가 필요합니다.
이 역할을 하는 것이 바로 반복자(Iterator) 입니다.
이번 장에서는 STL의 세 가지 주요 구성 요소 중 두 번째인 반복자에 대해 자세히 알아보겠습니다.
반복자는 컨테이너와 알고리즘을 연결하는 핵심적인 요소이며, STL의 유연성과 강력함을 가능하게 하는 중요한 개념입니다.
반복자(Iterator)란 무엇인가?
반복자는 컨테이너 내부의 요소들을 가리키고, 순회(traverse)하며, 접근할 수 있게 해주는 객체입니다.
포인터와 유사한 개념을 가지지만, 컨테이너의 종류에 따라 요소에 접근하는 방식을 일반화한 추상화된 개념입니다.
반복자의 역할
- 컨테이너 요소 접근:
*iterator
와 같이 역참조 연산자를 사용하여 반복자가 가리키는 요소의 값을 읽거나 쓸 수 있습니다. - 순회:
++iterator
와 같이 증감 연산자를 사용하여 다음 또는 이전 요소로 이동할 수 있습니다. - 범위 정의: 컨테이너의 시작(
begin()
)과 끝(end()
)을 나타내는 반복자를 통해 특정 범위의 요소를 지정할 수 있습니다.
왜 반복자가 필요한가? (추상화의 중요성)
만약 반복자가 없다면, 각 컨테이너 타입마다 요소를 순회하는 방식이 달라질 것입니다.
std::vector
는 인덱스를 사용한for
루프 (for (int i = 0; i < vec.size(); ++i)
)std::list
는 노드의next
포인터를 따라가는 방식 (Node* current = head; while(current) { ... current = current->next; }
)
이처럼 컨테이너마다 접근 방식이 다르다면, std::sort
와 같은 알고리즘을 구현할 때 vector
용, list
용 등 모든 컨테이너 타입에 대해 별도의 정렬 함수를 만들어야 할 것입니다.
하지만 반복자는 이러한 복잡성을 추상화하여, 어떤 컨테이너든 동일한 방식으로 요소에 접근하고 순회할 수 있도록 합니다.
덕분에 하나의 알고리즘(std::sort
)이 다양한 컨테이너 타입에 대해 동작할 수 있습니다.
반복자의 종류와 기능
STL 반복자는 기능에 따라 여러 카테고리로 나뉩니다.
각 카테고리는 이전 카테고리의 모든 기능을 포함합니다.
-
입력 반복자 (Input Iterators)
- 읽기 전용: 요소를 한 번만 읽을 수 있습니다.
operator*
(읽기),operator++
(전위/후위 증감) 지원.- 파일 입력 스트림(
std::istream_iterator
) 등에서 사용됩니다.
-
출력 반복자 (Output Iterators)
- 쓰기 전용: 요소를 한 번만 쓸 수 있습니다.
operator*
(쓰기),operator++
(전위/후위 증감) 지원.- 파일 출력 스트림(
std::ostream_iterator
) 등에서 사용됩니다.
-
순방향 반복자 (Forward Iterators)
- 읽기/쓰기가 가능하며, 여러 번 순회할 수 있습니다.
- 단방향으로만 이동(
++
)할 수 있습니다. std::forward_list
가 제공하는 반복자입니다.
-
양방향 반복자 (Bidirectional Iterators)
- 순방향 반복자의 모든 기능에 더해, 양방향으로 이동(
++
,--
)할 수 있습니다. std::list
,std::set
,std::map
등의 컨테이너가 제공하는 반복자입니다.
- 순방향 반복자의 모든 기능에 더해, 양방향으로 이동(
-
임의 접근 반복자 (Random Access Iterators)
- 양방향 반복자의 모든 기능에 더해, 임의의 위치로 한 번에 이동할 수 있습니다.
- 포인터처럼
iterator + N
,iterator - N
,iterator[N]
등의 산술 연산과 비교 연산(>
,<
등)을 지원합니다. std::vector
,std::deque
, C-스타일 배열이 제공하는 반복자입니다.- 대부분의 STL 알고리즘은 임의 접근 반복자를 가정하고 최적화되어 있습니다.
반복자의 기본 사용법
대부분의 STL 컨테이너는 다음과 같은 멤버 함수를 통해 반복자를 얻을 수 있습니다.
begin()
: 컨테이너의 첫 번째 요소를 가리키는 반복자를 반환합니다.end()
: 컨테이너의 마지막 요소 바로 다음을 가리키는 반복자를 반환합니다. (범위의 끝을 나타내며, 이 반복자는 유효한 요소를 가리키지 않습니다.)cbegin()
,cend()
:const
반복자를 반환합니다. (C++11부터) 요소를 읽기만 할 때 사용합니다.rbegin()
,rend()
: 역방향 반복자(Reverse Iterator)를 반환합니다. 컨테이너를 역순으로 순회할 때 사용합니다.
#include <iostream>
#include <vector>
#include <list>
#include <string>
int main() {
// 1. std::vector와 반복자
std::vector<int> numbers = {10, 20, 30, 40, 50};
// 벡터의 시작 반복자 (임의 접근 반복자)
std::vector<int>::iterator it_vec = numbers.begin();
// 반복자 역참조하여 값 접근
std::cout << "First element of vector: " << *it_vec << std::endl; // 출력: 10
// 다음 요소로 이동
++it_vec;
std::cout << "Second element of vector: " << *it_vec << std::endl; // 출력: 20
// 임의 접근 (임의 접근 반복자만 가능)
it_vec = numbers.begin() + 3; // 첫 번째 요소에서 3칸 뒤 (즉, 인덱스 3)
std::cout << "Fourth element of vector (random access): " << *it_vec << std::endl; // 출력: 40
// 모든 요소 순회 (루프)
std::cout << "Vector elements (using iterator loop): ";
for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl; // 출력: 10 20 30 40 50
// C++11 이상 Range-based for loop (내부적으로 반복자 사용)
std::cout << "Vector elements (using range-based for): ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl; // 출력: 10 20 30 40 50
std::cout << "-------------------------------------------\n";
// 2. std::list와 반복자
std::list<std::string> fruits = {"Apple", "Banana", "Cherry", "Durian"};
// 리스트의 시작 반복자 (양방향 반복자)
std::list<std::string>::iterator it_list = fruits.begin();
std::cout << "First element of list: " << *it_list << std::endl; // 출력: Apple
// 다음 요소로 이동
++it_list;
std::cout << "Second element of list: " << *it_list << std::endl; // 출력: Banana
// 이전 요소로 이동 (양방향 반복자만 가능)
--it_list;
std::cout << "First element of list (moved back): " << *it_list << std::endl; // 출력: Apple
// 모든 요소 순회
std::cout << "List elements: ";
for (std::list<std::string>::iterator it = fruits.begin(); it != fruits.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl; // 출력: Apple Banana Cherry Durian
return 0;
}
const
반복자
컨테이너의 요소를 읽기만 하고 수정하지 않을 때는 const_iterator
를 사용하는 것이 좋습니다.
cbegin()
과 cend()
멤버 함수는 const_iterator
를 반환합니다.
#include <iostream>
#include <vector>
int main() {
std::vector<int> data = {1, 2, 3, 4, 5};
// const_iterator 사용 (요소 값 변경 불가)
for (std::vector<int>::const_iterator it = data.cbegin(); it != data.cend(); ++it) {
std::cout << *it << " ";
// *it = 10; // 컴파일 오류! const_iterator는 값 수정 불가
}
std::cout << std::endl;
return 0;
}
반복자의 무효화 (Iterator Invalidation)
STL 컨테이너를 사용할 때 가장 흔히 겪는 오류 중 하나가 반복자 무효화(Iterator Invalidation) 입니다.
컨테이너의 구조가 변경될 때 (예: 요소 삽입/삭제, 컨테이너 크기 변경) 이전에 얻었던 반복자들이 더 이상 유효하지 않게 되는 현상입니다.
무효화된 반복자를 사용하면 정의되지 않은 동작(Undefined Behavior)이 발생하며, 이는 프로그램 충돌로 이어질 수 있습니다.
std::vector
- 요소 삽입/삭제 시:
insert()
,erase()
,push_back()
(용량 초과 시),pop_back()
등의 연산은 대부분의 경우 반복자를 무효화합니다. 특히 중간에 삽입/삭제하면 뒤쪽의 모든 반복자가 무효화됩니다.push_back
도 내부적으로 재할당이 발생하면 모든 반복자가 무효화됩니다. clear()
: 모든 반복자를 무효화합니다.
- 요소 삽입/삭제 시:
std::list
- 요소 삽입/삭제 시: 해당 요소와 관련된 반복자만 무효화될 수 있습니다. 다른 반복자들은 일반적으로 유효합니다.
- 연관 컨테이너 (
std::map
,std::set
등)- 요소 삽입 시: 일반적으로 기존 반복자는 무효화되지 않습니다.
- 요소 삭제 시: 삭제된 요소에 대한 반복자만 무효화됩니다.
- 삽입/삭제 후에는 항상
begin()
또는find()
등을 통해 새로운 유효한 반복자를 얻는 것이 안전합니다.
#include <iostream>
#include <vector>
#include <algorithm> // std::find
int main() {
std::vector<int> numbers = {10, 20, 30, 40, 50};
// 1. find를 사용하여 30을 찾고 출력
std::vector<int>::iterator it = std::find(numbers.begin(), numbers.end(), 30);
if (it != numbers.end()) {
std::cout << "Found 30 at: " << *it << std::endl; // 출력: Found 30 at: 30
}
// 2. 벡터에 새로운 요소 추가 (재할당이 발생할 수 있음)
std::cout << "Adding 60 to vector...\n";
numbers.push_back(60); // 만약 capacity가 부족하면 재할당 발생, 모든 반복자 무효화
// 3. 무효화된 반복자를 사용하려는 시도 (위험!)
// it를 다시 사용하면 정의되지 않은 동작이 발생할 수 있음
// std::cout << "Using invalidated iterator: " << *it << std::endl; // 잠재적 오류
// 4. 안전한 방법: 작업 후 반복자를 다시 얻어야 함
it = std::find(numbers.begin(), numbers.end(), 30); // 다시 검색하여 유효한 반복자 얻음
if (it != numbers.end()) {
std::cout << "Found 30 again (after push_back): " << *it << std::endl; // 출력: Found 30 again (after push_back): 30
}
// erase() 사용 시 반환되는 새 반복자 활용
std::cout << "Erasing 20 from vector...\n";
std::vector<int>::iterator erase_it = std::find(numbers.begin(), numbers.end(), 20);
if (erase_it != numbers.end()) {
// erase()는 삭제된 요소 다음 위치를 가리키는 유효한 반복자를 반환
erase_it = numbers.erase(erase_it); // erase_it는 이제 30을 가리킴
std::cout << "Element after erased 20: " << *erase_it << std::endl; // 출력: Element after erased 20: 30
}
std::cout << "Vector after erase: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl; // 출력: Vector after erase: 10 30 40 50 60
return 0;
}
팁: 반복자 무효화 문제를 피하는 가장 좋은 방법 중 하나는 컨테이너의 구조를 변경하는 작업을 수행할 때마다 필요한 반복자를 다시 얻거나, Range-based for loop (C++11 이상)를 사용하여 간접적으로 반복자를 사용하는 것입니다.
이터레이터 어댑터 (Iterator Adapters)
STL은 다양한 이터레이터 어댑터를 제공하여 반복자의 동작을 변경하거나 확장할 수 있습니다.
std::reverse_iterator
: 컨테이너를 역순으로 순회하게 해줍니다. (rbegin()
,rend()
가 반환)std::insert_iterator
(std::back_inserter
,std::front_inserter
,std::inserter
): 특정 위치에 요소를 삽입하는 반복자입니다.std::move_iterator
(C++11부터): 요소를 복사하는 대신 이동시키는 반복자입니다.