본문으로 건너뛰기

안동민 개발노트

본문 시작

반복자

반복자로 서로 다른 컨테이너의 요소와 범위를 공통 방식으로 순회하고 종류·const·무효화 규칙을 익힙니다.

컨테이너는 데이터를 저장하는 역할을 하지만, 컨테이너에 저장된 데이터에 접근하고 조작하기 위해서는 특별한 도구가 필요합니다.

이 역할을 하는 것이 바로 반복자(Iterator)입니다.

이번 장에서는 STL의 세 가지 주요 구성 요소 중 두 번째인 반복자를 다룹니다.

반복자는 컨테이너와 알고리즘을 연결하는 핵심적인 요소이며, STL의 유연성과 강력함을 가능하게 하는 중요한 개념입니다.

HTML 다이어그램: /docs/cpp/ch12/ch12-2/1.html

반복자(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 알고리즘은 임의 접근 반복자를 가정하고 최적화되어 있습니다.
HTML 다이어그램: /docs/cpp/ch12/ch12-2/2.html

반복자의 기본 사용법

대부분의 STL 컨테이너는 다음과 같은 멤버 함수를 통해 반복자를 얻을 수 있습니다.

  • begin(): 컨테이너의 첫 번째 요소를 가리키는 반복자를 반환합니다.
  • end(): 컨테이너의 마지막 요소 바로 다음을 가리키는 반복자를 반환합니다. (범위의 끝을 나타내며, 이 반복자는 유효한 요소를 가리키지 않습니다.)
  • cbegin(), cend(): const 반복자를 반환합니다. (C++11부터) 요소를 읽기만 할 때 사용합니다.
  • rbegin(), rend(): 역방향 반복자(Reverse Iterator)를 반환합니다. 컨테이너를 역순으로 순회할 때 사용합니다.
std::vector와 std::list에서 반복자 사용하기
#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를 반환합니다.

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)

HTML 다이어그램: /docs/cpp/ch12/ch12-2/3.html

STL 컨테이너를 사용할 때 가장 흔히 겪는 오류 중 하나가 반복자 무효화(Iterator Invalidation)입니다.

컨테이너의 구조가 변경될 때 (예: 요소 삽입/삭제, 컨테이너 크기 변경) 이전에 얻었던 반복자들이 더 이상 유효하지 않게 되는 현상입니다.

무효화된 반복자를 사용하면 정의되지 않은 동작(Undefined Behavior)이 발생하며, 이는 프로그램 충돌로 이어질 수 있습니다.

  • std::vector
    • 요소 삽입/삭제 시: insert(), erase(), push_back() (용량 초과 시), pop_back() 등의 연산은 대부분의 경우 반복자를 무효화합니다. 특히 중간에 삽입/삭제하면 뒤쪽의 모든 반복자가 무효화됩니다. push_back도 내부적으로 재할당이 발생하면 모든 반복자가 무효화됩니다.
    • clear(): 모든 반복자를 무효화합니다.
  • std::list
    • 요소 삽입/삭제 시: 해당 요소와 관련된 반복자만 무효화될 수 있습니다. 다른 반복자들은 일반적으로 유효합니다.
  • 연관 컨테이너 (std::map, std::set 등)
    • 요소 삽입 시: 일반적으로 기존 반복자는 무효화되지 않습니다.
    • 요소 삭제 시: 삭제된 요소에 대한 반복자만 무효화됩니다.
    • 삽입/삭제 후에는 항상 begin() 또는 find() 등을 통해 새로운 유효한 반복자를 얻는 것이 안전합니다.
std::vector에서 반복자 무효화
#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 이상)를 사용하여 간접적으로 반복자를 사용하는 것입니다.

HTML 다이어그램: /docs/cpp/ch12/ch12-2/4.html

이터레이터 어댑터 (Iterator Adapters)

STL은 다양한 이터레이터 어댑터를 제공하여 반복자의 동작을 변경하거나 확장할 수 있습니다.

  • std::reverse_iterator: 컨테이너를 역순으로 순회하게 해줍니다. (rbegin(), rend()가 반환)
  • std::insert_iterator (std::back_inserter, std::front_inserter, std::inserter): 특정 위치에 요소를 삽입하는 반복자입니다.
  • std::move_iterator (C++11부터): 요소를 복사하는 대신 이동시키는 반복자입니다.

마무리 점검 다이어그램

HTML 다이어그램: /docs/cpp/ch12/ch12-2/5.html

반복자는 begin/end 범위와 무효화 규칙을 같이 알아야 안전하게 순회할 수 있습니다.

HTML 다이어그램: /docs/cpp/ch12/ch12-2/8.html