C++ 반복자 기초 완벽 가이드 | iterator 카테고리·begin/end·역방향 반복자·실전 패턴

C++ 반복자 기초 완벽 가이드 | iterator 카테고리·begin/end·역방향 반복자·실전 패턴

이 글의 핵심

C++ 반복자 기초 완벽 가이드에 대한 실전 가이드입니다. iterator 카테고리·begin/end·역방향 반복자·실전 패턴 등을 예제와 함께 상세히 설명합니다.

들어가며: 순회 중 erase했는데 프로그램이 죽어요

”반복자로 순회하다가 erase 호출 후 크래시가 나요”

반복자(iterator)는 STL 컨테이너의 원소를 순회·접근하는 객체입니다. 포인터처럼 *it로 역참조, ++it로 다음 원소로 이동, it != end()로 비교할 수 있습니다. STL 알고리즘은 모두 [begin, end) 반복자 범위를 받아 동작하므로, 반복자를 이해해야 STL을 제대로 활용할 수 있습니다.

비유하면 반복자는 “책장의 책갈피”입니다. 책갈피를 한 칸씩 옮기면서 페이지를 읽듯, 반복자를 ++로 이동하면서 원소에 접근합니다. end()는 “마지막 페이지 다음”을 가리켜 미포함 범위 [begin, end)를 표현합니다.

문제의 코드:

// ❌ 나쁜 예: erase 후 반복자 무효화
std::vector<int> vec = {1, 2, 0, 3, 0, 4};
for (auto it = vec.begin(); it != vec.end(); ++it) {
    if (*it == 0) vec.erase(it);  // it 무효화 → 다음 ++it에서 미정의 동작!
}

위 코드 설명: vec.erase(it)it가 가리키던 원소를 제거하고, 그 이후의 모든 반복자를 무효화합니다. ++it를 하면 이미 무효화된 반복자를 사용해 미정의 동작이 됩니다.

해결법:

// ✅ 올바른 사용: erase가 반환하는 새 반복자 사용
for (auto it = vec.begin(); it != vec.end(); ) {
    if (*it == 0) it = vec.erase(it);
    else ++it;
}

// ✅ 또는 erase-remove idiom
vec.erase(std::remove(vec.begin(), vec.end(), 0), vec.end());

이 글을 읽으면:

  • 반복자 카테고리와 begin/end의 의미를 이해할 수 있습니다.
  • 역방향 반복자·const 반복자·범위 기반 for와의 관계를 알 수 있습니다.
  • 자주 겪는 에러와 해결법을 배울 수 있습니다.
  • 프로덕션에서 검증된 패턴을 활용할 수 있습니다.
flowchart TB
  subgraph problem["자주 겪는 실패 시나리오"]
    P1[erase 후 반복자 무효화]
    P2[end 역참조 → UB]
    P3[순회 중 push_back]
    P4[역방향 반복자 base 잘못 사용]
  end
  subgraph solution["올바른 선택"]
    S1[erase 반환값 사용]
    S2[it != end 검사 후 역참조]
    S3[순회 중 수정 금지]
    S4[reverse_iterator.base 사용법]
  end
  P1 --> S1
  P2 --> S2
  P3 --> S3
  P4 --> S4

목차

  1. 문제 시나리오와 원인 분석
  2. 반복자 카테고리
  3. begin과 end 완전 가이드
  4. 역방향 반복자 (reverse_iterator)
  5. 반복자 어댑터
  6. std::distance와 std::advance
  7. 완전한 반복자 예제 모음
  8. 커스텀 반복자 구현
  9. 자주 발생하는 에러와 해결법
  10. 베스트 프랙티스
  11. 프로덕션 패턴
  12. 구현 체크리스트

1. 문제 시나리오와 원인 분석

시나리오 1: erase 루프에서 반복자 무효화

증상: 0을 제거하는 루프에서 vec.erase(it)++it 시 크래시.

원인: erase는 해당 원소를 제거하고, 그 위치 이후의 반복자들이 무효화됩니다. erase삭제된 원소의 다음을 가리키는 반복자를 반환하므로, it = vec.erase(it)로 받아야 합니다.

해결: it = vec.erase(it) 또는 erase-remove idiom 사용.

시나리오 2: find 반환값을 end()와 비교하지 않음

증상: *std::find(...)로 역참조 시 크래시.

원인: find는 값을 찾지 못하면 end()를 반환합니다. end()는 “마지막 원소 다음”을 가리키므로 역참조하면 미정의 동작입니다.

해결: auto it = std::find(...); if (it != vec.end()) { /* *it 사용 */ }

시나리오 3: 범위 기반 for 안에서 컨테이너 수정

증상: for (auto& x : vec) 루프 안에서 vec.push_back() 호출 시 크래시.

원인: 범위 기반 for는 내부적으로 begin/end를 사용합니다. 순회 중 push_back/insert/erase는 반복자를 무효화합니다.

해결: 순회 중에는 컨테이너를 수정하지 않거나, 인덱스 기반 루프 또는 erase-remove idiom 사용.

시나리오 4: 역방향 반복자에서 base() 잘못 사용

증상: reverse_iteratorerase에 넘기려 할 때 잘못된 원소가 삭제됨.

원인: reverse_iterator::base()는 “역방향으로 보던 마지막 원소의 다음”을 가리킵니다. rit.base()rit가 가리키는 논리적 위치가 다릅니다.

해결: vec.erase(std::next(rit).base()) 또는 vec.erase((++rit).base())로 삭제할 원소에 맞는 반복자 사용.

시나리오 5: const 반복자 vs 비const 반복자 혼용

증상: cbegin()/cend()로 얻은 반복자로 *it = 42 시 컴파일 에러.

원인: cbegin()/cend()는 const 반복자를 반환합니다. const 반복자로는 원소를 수정할 수 없습니다.

해결: 읽기 전용이면 cbegin/cend, 수정이 필요하면 begin/end 사용.

시나리오 6: 빈 컨테이너에서 begin == end

증상: 빈 vector에서 *vec.begin() 역참조 시 크래시.

원인: 빈 컨테이너에서는 begin() == end()입니다. begin()을 역참조하면 미정의 동작입니다.

해결: if (!vec.empty()) 또는 it != vec.end() 검사 후 역참조.

시나리오 7: std::distance에 역순 범위

증상: std::distance(last, first) 미정의 동작.

해결: std::distance(vec.begin(), it)로 항상 순방향 범위 전달.

시나리오 8: back_inserter 없이 빈 벡터에 copy

증상: std::copy(src.begin(), src.end(), dst.begin())에서 dst 비어 있으면 크래시.

해결: std::back_inserter(dst) 사용 또는 dst.resize(src.size()) 후 복사.

시나리오 9: map/set 순회 중 erase

증상: map/set 순회 중 erase(it)++it 시 크래시.

원인: erase(it)는 반복자를 무효화합니다. it = m.erase(it)로 반환값을 받아야 합니다.

해결: it = m.erase(it) 사용. C++20에서는 std::erase_if(m, pred) 활용.


2. 반복자 카테고리

C++ 표준은 반복자 카테고리를 계층적으로 정의합니다. 각 카테고리는 지원하는 연산이 다르며, 알고리즘은 “필요한 최소 카테고리”만 요구합니다.

카테고리 계층 구조

flowchart TD
    Input["Input Iteratorbr/읽기, 전진, 1회만"]
    Output["Output Iteratorbr/쓰기, 전진, 1회만"]
    Forward["Forward Iteratorbr/읽기/쓰기, 전..."]
    Bidirectional["Bidirectional Iteratorbr/전진..."]
    RandomAccess["Random Access Iteratorbr/임의..."]
    Input --> Forward
    Output --> Forward
    Forward --> Bidirectional
    Bidirectional --> RandomAccess

위 다이어그램 설명: Input/Output은 1회 스캔만 가능하고, Forward는 다회 순회, Bidirectional은 -- 지원, Random Access는 it + n, it[n] 지원합니다.

카테고리별 지원 연산

카테고리지원 연산예시 컨테이너
Input*it, ++it, it == it2istream_iterator
Output*it = x, ++itostream_iterator, back_inserter
ForwardInput + 다회 순회forward_list, unordered_*
BidirectionalForward + --itlist, map, set
Random AccessBidirectional + it + n, it[n], <vector, deque, array

카테고리 확인 예제

#include <iterator>
#include <vector>
#include <list>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3};
    std::list<int> lst = {1, 2, 3};

    // vector: Random Access
    auto v_it = vec.begin();
    v_it += 2;           // OK
    int x = v_it[0];    // OK
    std::cout << "vector: " << x << "\n";

    // list: Bidirectional (Random Access 아님)
    auto l_it = lst.begin();
    ++l_it; ++l_it;     // OK
    // l_it += 2;       // 에러: list 반복자는 += 미지원
    std::cout << "list: " << *l_it << "\n";
}

위 코드 설명: vector는 연속 메모리이므로 it + n, it[n]이 O(1)입니다. list는 노드 기반이므로 +=가 없고, ++/--만 O(1)입니다. std::sort는 Random Access를 요구하므로 list에는 sort 멤버 함수를 사용해야 합니다.

iterator_traits로 카테고리 확인

#include <iterator>
#include <vector>
#include <list>
#include <type_traits>

int main() {
    using VIt = std::vector<int>::iterator;
    using LIt = std::list<int>::iterator;

    // iterator_category: Random Access vs Bidirectional
    static_assert(std::is_same_v<
        std::iterator_traits<VIt>::iterator_category,
        std::random_access_iterator_tag
    >);
    static_assert(std::is_same_v<
        std::iterator_traits<LIt>::iterator_category,
        std::bidirectional_iterator_tag
    >);
}

위 코드 설명: std::iterator_traits<It>::iterator_category로 해당 반복자의 카테고리를 컴파일 타임에 확인할 수 있습니다. 알고리즘 오버로딩이나 SFINAE에 활용됩니다.


3. begin과 end 완전 가이드

반개구간 [begin, end)

STL의 모든 범위는 반개구간(half-open range) [begin, end)를 사용합니다. begin은 첫 원소를, end마지막 원소의 다음을 가리킵니다. end는 역참조하면 안 됩니다.

#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {10, 20, 30};

    // [begin, end) 시각화
    // vec:  [10] [20] [30]
    //        ^         ^
    //      begin     end (역참조 금지)

    auto it = vec.begin();
    std::cout << *it << "\n";  // 10
    ++it;
    std::cout << *it << "\n";  // 20
    ++it;
    std::cout << *it << "\n";  // 30
    ++it;
    // it == vec.end() → true
    // *it;  // ❌ 미정의 동작
}

위 코드 설명: [begin, end)는 “begin 포함, end 미포함”입니다. 원소 개수는 std::distance(begin, end) 또는 vec.size()와 같습니다. 빈 범위면 begin == end입니다.

begin() / end() vs std::begin() / std::end()

#include <vector>
#include <array>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3};
    std::array<int, 3> arr = {4, 5, 6};
    int c_arr[] = {7, 8, 9};

    // 멤버 함수 (컨테이너)
    auto v_b = vec.begin();
    auto v_e = vec.end();

    // std::begin/end (C 배열과 C++ 컨테이너 모두)
    auto a_b = std::begin(arr);
    auto a_e = std::end(arr);
    auto c_b = std::begin(c_arr);
    auto c_e = std::end(c_arr);

    std::cout << *v_b << " " << *a_b << " " << *c_b << "\n";  // 1 4 7
}

위 코드 설명: std::begin/std::end는 C 배열과 C++ 컨테이너 모두에 동작합니다. 제네릭 코드에서는 std::begin(r)/std::end(r)를 사용하면 배열·벡터·커스텀 타입을 모두 지원할 수 있습니다.

cbegin() / cend() — 읽기 전용

#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3};

    auto it = vec.cbegin();
    int x = *it;   // OK: 읽기
    // *it = 42;   // 에러: const 반복자로 수정 불가

    auto it2 = vec.begin();
    *it2 = 42;     // OK: 비const 반복자
}

위 코드 설명: cbegin/cendconst_iterator를 반환합니다. 원소를 수정하지 않는 읽기 전용 순회에 사용하면, 실수로 수정하는 것을 방지할 수 있습니다.

빈 컨테이너 처리

#include <vector>
#include <algorithm>

void process(const std::vector<int>& vec) {
    if (vec.empty()) return;  // 또는
    if (vec.begin() == vec.end()) return;

    auto it = std::find(vec.begin(), vec.end(), 42);
    if (it != vec.end()) {
        // *it 사용
    }
}

위 코드 설명: 빈 컨테이너에서는 begin() == end()이므로, find 등은 end()를 반환합니다. 역참조 전에 항상 it != end()를 확인하세요.

범위 기반 for와 begin/end

#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3};

    // 범위 기반 for는 내부적으로 begin/end 사용
    for (auto& x : vec) {
        x *= 2;  // 수정 가능 (auto&)
    }

    // const 순회
    for (const auto& x : vec) {
        // x 수정 불가
    }
}

위 코드 설명: for (auto& x : range)begin(range)에서 end(range) 직전까지 순회합니다. rangestd::initializer_list이면 begin/end가 자동으로 호출됩니다.


4. 역방향 반복자 (reverse_iterator)

rbegin() / rend() 기본 사용

역방향 반복자는 끝에서 처음으로 순회합니다. rbegin()은 마지막 원소를, rend()는 첫 원소의 을 가리킵니다.

#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    // 역순 출력: 5 4 3 2 1
    for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
        std::cout << *rit << " ";
    }
    std::cout << "\n";

    // std::reverse_copy와 동일한 효과
    std::vector<int> reversed(vec.rbegin(), vec.rend());
    // reversed: {5, 4, 3, 2, 1}
}

위 코드 설명: ++rit는 역방향으로 한 칸 이동합니다(즉, 논리적으로 앞으로). rbegin()end()의 앞쪽, rend()begin()의 앞쪽에 대응됩니다.

reverse_iterator와 base()

reverse_iterator::base()는 “역방향으로 보던 마지막 원소의 다음”을 가리키는 일반 반복자를 반환합니다. 따라서 rit가 가리키는 원소를 지우려면 (std::next(rit)).base()를 사용해야 합니다.

#include <vector>
#include <iterator>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    int target = 3;
    auto rit = std::find(vec.rbegin(), vec.rend(), target);

    if (rit != vec.rend()) {
        // rit가 가리키는 3을 지우려면: base()의 "앞" 위치
        // rit.base() = 3 다음 (4를 가리킴)
        // (++rit).base() = 3을 가리킴
        vec.erase((++rit).base());
        // 또는 vec.erase(std::next(rit).base());
    }
    // vec: {1, 2, 4, 5}
}

위 코드 설명: reverse_iterator가 가리키는 논리적 위치와 base()가 가리키는 위치는 한 칸 어긋나 있습니다. rit가 가리키는 원소를 지우려면 (++rit).base()erase에 넘깁니다.

역방향 순회 다이어그램

flowchart LR
    subgraph forward["정방향 (begin, end)"]
        B[begin] --> E[end]
    end
    subgraph reverse["역방향 (rbegin, rend)"]
        RB[rbegin] --> RE[rend]
    end
    B -.->|대응| RE
    E -.->|대응| RB

위 다이어그램 설명: rbegin()end()-1의 논리적 위치(마지막 원소)를, rend()begin()의 앞(첫 원소 앞)을 가리킵니다. ++rit는 정방향으로 보면 --에 해당합니다.

crbegin() / crend() — const 역방향

#include <vector>

void print(const std::vector<int>& vec) {
    for (auto rit = vec.crbegin(); rit != vec.crend(); ++rit) {
        std::cout << *rit << " ";  // 읽기만
        // *rit = 0;  // 에러
    }
}

위 코드 설명: crbegin/crend는 const 역방향 반복자를 반환합니다. 읽기 전용 역순 순회에 사용합니다.


5. 반복자 어댑터

반복자 어댑터는 다른 반복자나 컨테이너를 감싸서 새로운 동작을 제공합니다. STL 알고리즘에 “출력 대상”을 지정할 때 자주 사용합니다.

back_inserter — 벡터 끝에 추가

#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};
    std::vector<int> dst;

    // std::copy가 dst.end()에 쓰려 하면 UB
    // back_inserter는 push_back을 호출해 안전하게 추가
    std::copy(src.begin(), src.end(), std::back_inserter(dst));

    for (int x : dst) std::cout << x << " ";  // 1 2 3 4 5
}

위 코드 설명: std::back_inserter(dst)std::back_insert_iterator를 반환합니다. *it = valuedst.push_back(value)를 호출합니다. dst 크기를 미리 할당할 필요가 없습니다.

front_inserter — 리스트 앞에 삽입

#include <list>
#include <algorithm>
#include <iterator>
std::vector<int> src = {1, 2, 3};
std::list<int> lst;
std::copy(src.begin(), src.end(), std::front_inserter(lst));
// lst: {3, 2, 1} — 역순 삽입

위 코드 설명: front_inserterpush_front를 사용합니다. list, deque만 지원. vector는 사용 불가.

insert_iterator — 특정 위치에 삽입

#include <vector>
#include <algorithm>
#include <iterator>
std::vector<int> src = {10, 20, 30};
std::vector<int> dst = {1, 2, 3};
auto it = std::find(dst.begin(), dst.end(), 2);
std::copy(src.begin(), src.end(), std::inserter(dst, it));
// dst: {1, 10, 20, 30, 2, 3}

위 코드 설명: std::inserter(container, pos)pos 위치에 insert를 호출합니다.

ostream_iterator — 스트림 출력

#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    std::copy(vec.begin(), vec.end(),
              std::ostream_iterator<int>(std::cout, "\n"));

    // 출력: 1\n2\n3\n4\n5
}

위 코드 설명: ostream_iterator<T>(stream, delim)*it = valuestream << value << delim를 호출합니다. 알고리즘 결과를 바로 출력할 때 유용합니다.

istream_iterator — 스트림 입력

#include <vector>
#include <iterator>
std::vector<int> vec(std::istream_iterator<int>(std::cin),
                     std::istream_iterator<int>());  // EOF까지

위 코드 설명: istream_iterator<T>()는 EOF를 나타냅니다. cin에서 EOF까지 읽어 vector를 채웁니다.

어댑터용도지원 컨테이너
back_inserter끝에 추가vector, deque, list, string
front_inserter앞에 삽입list, deque
inserter특정 위치 삽입대부분 STL 컨테이너
ostream_iterator스트림 출력cout, ofstream
istream_iterator스트림 입력cin, ifstream

6. std::distance와 std::advance

반복자 이동과 거리 계산에 std::advancestd::distance를 사용하면 컨테이너 종류에 관계없이 동작하는 제네릭 코드를 작성할 수 있습니다.

std::advance — 반복자 n칸 이동

#include <iterator>
#include <vector>
#include <list>
std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<int> lst = {1, 2, 3, 4, 5};

auto v_it = vec.begin();
std::advance(v_it, 3);  // vector: O(1)
auto l_it = lst.begin();
std::advance(l_it, 3);  // list: O(n), ++ 루프
std::advance(l_it, -2);  // 음수: -- 로 역방향

위 코드 설명: std::advance(it, n)itn칸 이동합니다. Random Access면 O(1), Bidirectional면 O(n)입니다.

std::distance — 두 반복자 사이 거리

#include <iterator>
#include <vector>
#include <algorithm>
std::vector<int> vec = {10, 20, 30, 40, 50};

auto dist = std::distance(vec.begin(), vec.end());  // 5

auto it = std::find(vec.begin(), vec.end(), 40);
if (it != vec.end()) {
    size_t idx = std::distance(vec.begin(), it);  // 3
}

위 코드 설명: std::distance(first, last)[first, last) 원소 개수를 반환합니다. Random Access면 O(1), 그 외 O(n). firstlast보다 뒤면 미정의 동작입니다.

std::next와 std::prev

#include <iterator>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};

    // advance: it 자체 수정 (void)
    auto it1 = vec.begin();
    std::advance(it1, 2);

    // next: 원본 유지, 새 반복자 반환
    auto it2 = std::next(vec.begin(), 2);

    // prev: 역방향 next
    auto it3 = std::prev(vec.end(), 1);  // 마지막 원소
}

위 코드 설명: std::next(it, n)it를 수정하지 않고 n칸 이동한 복사본을 반환합니다. std::prev(it, n)은 역방향 이동입니다.


7. 완전한 반복자 예제 모음

예제 1: begin/end로 모든 컨테이너 순회

#include <vector>
#include <list>
#include <array>
#include <iostream>

template<typename Container>
void print(const Container& c) {
    for (auto it = std::begin(c); it != std::end(c); ++it)
        std::cout << *it << " ";
    std::cout << "\n";
}

int main() {
    std::vector<int> vec = {1, 2, 3};
    std::list<int> lst = {4, 5, 6};
    int c_arr[] = {10, 11, 12};
    print(vec);
    print(lst);
    print(c_arr);
}

위 코드 설명: std::begin/std::end로 템플릿 하나로 모든 컨테이너와 C 배열을 처리할 수 있습니다.

예제 2: erase 루프 (조건부 삭제)

#include <vector>
std::vector<int> vec = {1, 0, 2, 0, 3, 0, 4};
for (auto it = vec.begin(); it != vec.end(); ) {
    if (*it == 0) it = vec.erase(it);
    else ++it;
}
// vec: {1, 2, 3, 4}

위 코드 설명: erase는 삭제된 원소의 다음 반복자를 반환합니다. it = vec.erase(it)로 받아야 합니다.

예제 3: 역방향으로 특정 값 찾아 삭제

#include <vector>
#include <algorithm>
#include <iterator>
std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};
auto rit = std::find(vec.rbegin(), vec.rend(), 2);
if (rit != vec.rend())
    vec.erase((++rit).base());  // 마지막 2 제거

위 코드 설명: findrbegin/rend를 넘기면 역방향으로 첫 번째(마지막에 있는) target을 반환합니다. (++rit).base()로 해당 원소를 가리키는 반복자를 얻습니다.

예제 4: iterator_traits 활용

#include <iterator>
#include <vector>
#include <list>

template<typename Iterator>
void advance_if_random_access(Iterator& it, int n) {
    if constexpr (std::is_same_v<
            typename std::iterator_traits<Iterator>::iterator_category,
            std::random_access_iterator_tag>)
        it += n;
    else {
        if (n >= 0) while (n--) ++it;
        else while (n++) --it;
    }
}
// vector: O(1), list: O(n)

위 코드 설명: iterator_traits로 카테고리를 확인해 Random Access면 +=, 아니면 ++/-- 루프를 사용합니다.

예제 5: 부분 범위 처리

#include <vector>
#include <algorithm>
std::vector<int> vec = {5, 2, 8, 1, 9, 3, 7};
std::sort(vec.begin(), vec.begin() + 3);  // 앞 3개만
auto it = std::find(vec.begin() + 2, vec.begin() + 5, 9);
if (it != vec.begin() + 5) {
    auto idx = std::distance(vec.begin(), it);  // 인덱스
}

위 코드 설명: vec.begin() + n으로 부분 범위 지정. Random Access에서만 + 지원. std::distance로 인덱스 계산.

예제 6: const 반복자로 읽기 전용 API

#include <vector>
#include <algorithm>
bool contains(const std::vector<int>& vec, int value) {
    return std::find(vec.cbegin(), vec.cend(), value) != vec.cend();
}
int sum(const std::vector<int>& vec) {
    int s = 0;
    for (auto it = vec.cbegin(); it != vec.cend(); ++it) s += *it;
    return s;
}

위 코드 설명: const 참조 함수에서는 cbegin/cend로 수정 불가를 명시합니다.


8. 커스텀 반복자 구현

자체 컨테이너나 특수한 순회 로직이 필요할 때 커스텀 반복자를 구현할 수 있습니다. iterator_traits를 특수화하거나 std::iterator_traits가 자동 추론할 수 있도록 필요한 타입을 정의합니다.

최소 요구사항 (Random Access Iterator)

#include <iterator>
#include <algorithm>
#include <iostream>

template<typename T, size_t N>
class FixedArray {
public:
    T data[N];

    class Iterator {
    public:
        using value_type = T;
        using difference_type = std::ptrdiff_t;
        using iterator_category = std::random_access_iterator_tag;
        using pointer = T*;
        using reference = T&;

        explicit Iterator(T* ptr) : ptr_(ptr) {}
        reference operator*() const { return *ptr_; }
        Iterator& operator++() { ++ptr_; return *this; }
        Iterator& operator--() { --ptr_; return *this; }
        Iterator& operator+=(difference_type n) { ptr_ += n; return *this; }
        Iterator operator+(difference_type n) const { return Iterator(ptr_ + n); }
        Iterator operator-(difference_type n) const { return Iterator(ptr_ - n); }
        difference_type operator-(const Iterator& other) const { return ptr_ - other.ptr_; }
        bool operator==(const Iterator& other) const { return ptr_ == other.ptr_; }
        bool operator!=(const Iterator& other) const { return ptr_ != other.ptr_; }
        bool operator<(const Iterator& other) const { return ptr_ < other.ptr_; }
    private:
        T* ptr_;
    };

    Iterator begin() { return Iterator(data); }
    Iterator end() { return Iterator(data + N); }
};

int main() {
    FixedArray<int, 5> arr = {{1, 2, 3, 4, 5}};
    std::sort(arr.begin(), arr.end());
    auto it = std::find(arr.begin(), arr.end(), 3);
    if (it != arr.end())
        std::cout << "Index: " << std::distance(arr.begin(), it) << "\n";
}

위 코드 설명: iterator_category, value_type, difference_type 등을 정의하면 std::iterator_traits가 자동으로 사용합니다. begin/end만 제공하면 범위 기반 for와 STL 알고리즘을 사용할 수 있습니다.

범위 기반 for 지원

// begin/end만 정의하면 범위 기반 for 자동 지원
for (int x : arr) {
    std::cout << x << " ";
}

위 코드 설명: begin()end()를 제공하면 for (auto x : range)가 자동으로 동작합니다. std::begin/std::end도 이 멤버를 호출합니다.


9. 자주 발생하는 에러와 해결법

에러 1: erase 후 반복자 무효화

증상: vec.erase(it)++it 시 크래시.

해결법:

// ❌ for (auto it = ...; ++it) { if (*it==0) vec.erase(it); }
// ✅ for (auto it = ...; ) { if (*it==0) it=vec.erase(it); else ++it; }

에러 2: find 반환값을 end()와 비교하지 않음

증상: *std::find(...) 역참조 시 크래시 (없으면 end() 반환).

해결법:

// ❌ int value = *std::find(...);
// ✅ auto it = std::find(...); if (it != vec.end()) use(*it);

에러 3: 범위 기반 for 안에서 컨테이너 수정

증상: for (auto& x : vec) 안에서 vec.push_back() 시 크래시.

해결법: 순회 중 수정 금지. 별도 컨테이너에 모은 뒤 insert로 일괄 추가.

에러 4: reverse_iterator의 base() 잘못 사용

증상: rit가 가리키는 원소를 지우려 했는데 다른 원소가 삭제됨.

해결법: rit.base()는 rit 다음을 가리킴 → vec.erase((++rit).base()) 사용.

에러 5: 빈 컨테이너에서 begin() 역참조

증상: 빈 vector에서 *vec.begin() 시 크래시.

원인: 빈 컨테이너에서는 begin() == end()입니다.

해결법:

// ❌ 잘못된 사용
std::vector<int> vec;
int x = *vec.begin();  // UB

// ✅ 올바른 사용
if (!vec.empty()) {
    int x = *vec.begin();
}

에러 6: list에 std::sort 사용

증상: std::sort(lst.begin(), lst.end()) 컴파일 에러.

해결법: list는 Bidirectional → lst.sort() 멤버 함수 사용.

에러 7: 반복자 범위 [begin, end) 혼동

증상: end를 포함해 처리하려다 범위 초과.

해결법: [begin, end)는 end 미포함. end 역참조 금지.

에러 8: 반복자 복사 후 원본 컨테이너 수정

증상: 반복자 저장 후 push_back 호출, 이후 사용 시 크래시.

해결법: push_back/insert 후 반복자 재획득.

에러 9~11: 어댑터·distance·연산자

  • 9. vector에 front_inserter: vectorpush_front 없음 → list/deque 사용.
  • 10. distance 역순: distance(it, begin) 미정의 → distance(begin, it) 사용.
  • 11. list에 it + n: list는 Bidirectional → std::next(it, n) 사용.

10. 베스트 프랙티스

1. 반복자 범위 [begin, end) 일관 사용

// ✅ begin, end 쌍으로 범위 전달
std::sort(vec.begin(), vec.end());

// ✅ 부분 범위
std::sort(vec.begin() + 2, vec.end() - 1);

2. 읽기 전용이면 cbegin/cend 사용

// ✅ const 순회
for (auto it = vec.cbegin(); it != vec.cend(); ++it) {
    process(*it);
}

3. 역참조 전 end() 검사

// ✅ find 반환값 검사
auto it = std::find(vec.begin(), vec.end(), value);
if (it != vec.end()) {
    use(*it);
}

4. 제네릭 코드에서는 std::begin/end

template<typename Range>
void process(Range& r) {
    for (auto it = std::begin(r); it != std::end(r); ++it) {
        // C 배열, vector, array 등 모두 지원
    }
}

5. erase-remove idiom 선호

// ✅ 조건 삭제 시 erase-remove
vec.erase(std::remove_if(vec.begin(), vec.end(),
     { return x == 0; }), vec.end());

6. iterator_traits로 카테고리 확인

template<typename It>
void advance_impl(It& it, int n, std::random_access_iterator_tag) {
    it += n;
}
template<typename It>
void advance_impl(It& it, int n, std::bidirectional_iterator_tag) {
    while (n > 0) { ++it; --n; }
    while (n < 0) { --it; ++n; }
}

7. 제네릭 이동·출력

std::advance(it, n);  // 컨테이너 무관
auto mid = std::next(vec.begin(), vec.size() / 2);
std::transform(src.begin(), src.end(), std::back_inserter(result), f);

11. 프로덕션 패턴

패턴 1: 안전한 erase 루프

template<typename Container, typename Predicate>
void erase_if(Container& c, Predicate pred) {
    for (auto it = c.begin(); it != c.end(); ) {
        if (pred(*it)) it = c.erase(it);
        else ++it;
    }
}

패턴 2: 역방향 검색 후 삭제

// 마지막으로 나오는 target 제거
auto rit = std::find(vec.rbegin(), vec.rend(), target);
if (rit != vec.rend()) {
    vec.erase((++rit).base());
}

패턴 3: 반복자 유효성 검사

bool is_valid(const std::vector<int>& vec,
              std::vector<int>::const_iterator it) {
    return it >= vec.begin() && it < vec.end();
}

패턴 4: 부분 범위 알고리즘

std::partial_sort(vec.begin(), vec.begin() + 100, vec.end());
auto mid = std::next(vec.begin(), vec.size() / 2);
std::transform(vec.begin(), mid, result.begin(), f);

패턴 5: const 반복자로 API 설계

class DataStore {
public:
    auto begin() const { return data_.cbegin(); }
    auto end() const { return data_.cend(); }
private:
    std::vector<int> data_;
};

패턴 6: 빈 범위 처리

template<typename Range>
void safe_process(Range& r) {
    if (std::begin(r) == std::end(r)) return;
    // ...
}

12. 구현 체크리스트

반복자 사용 체크리스트

  • 역참조 전 it != end() 검사하는가?
  • erase 후 반환값을 받아 it = vec.erase(it) 하는가?
  • 순회 중에는 컨테이너를 수정하지 않는가?
  • reverse_iterator 삭제 시 (++rit).base() 사용하는가?
  • 읽기 전용이면 cbegin/cend 사용하는가?

에러 방지 체크리스트

  • 빈 컨테이너에서 begin() 역참조하지 않는가?
  • reverse_iterator::base() 직접 erase에 넘기지 않는가?
  • liststd::sort 대신 std::list::sort 사용하는가?
  • 반복자 저장 후 push_back/insert 시 재획득하는가?

실무 팁

개발 시 주의사항

  1. [팁 1]: [설명]

    // 예시 코드
  2. [팁 2]: [설명]

    // 예시 코드
  3. [팁 3]: [설명]

디버깅 방법

  • [방법 1]: [설명]
  • [방법 2]: [설명]
  • [방법 3]: [설명]

FAQ

Q: “begin과 end 중 end는 왜 역참조하면 안 되나요?”

A: end()는 “마지막 원소의 다음”을 가리킵니다. 유효한 원소가 없으므로 역참조하면 미정의 동작입니다. [begin, end) 반개구간으로 “첫 원소부터 마지막 원소까지”를 표현합니다.

Q: “vector와 list의 반복자 차이는?”

A: vector는 Random Access(it + n, it[n] 지원), list는 Bidirectional(++, --만). std::sort는 Random Access를 요구하므로 list에는 lst.sort() 멤버 함수를 사용합니다.

Q: “범위 기반 for와 반복자, 뭘 쓰나요?”

A: 단순 순회면 for (auto& x : vec)가 간결합니다. erase·인덱스·조건부 삭제가 필요하면 반복자 루프를 사용하세요.

Q: “reverse_iterator.base()가 헷갈려요.”

A: rit가 가리키는 원소를 지우려면 (++rit).base()를 사용합니다. rit.base()rit가 가리키는 원소의 다음을 가리킵니다.

Q: “프로덕션에서 주의할 점은?”

A: erase 반환값 사용, find 반환값 end() 비교, 순회 중 수정 금지, reverse_iterator base 사용법, 빈 컨테이너 처리.


참고 자료


한 줄 요약: 반복자는 STL의 핵심. begin/end는 [begin, end) 반개구간이고, erase 후 반환값 사용, find 반환값 end() 비교, 순회 중 수정 금지, reverse_iterator base 사용법을 지키면 안전합니다. 다음으로 STL 알고리즘 기초를 읽어보면 좋습니다.

이전 글: C++ vector 기초 | 초기화·연산·용량 관리와 실전 패턴
다음 글: C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ vector 기초 완벽 가이드 | 초기화·연산·용량 관리와 실전 패턴
  • C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
  • C++ 범위 기반 for문과 구조화된 바인딩 | 모던 C++ 반복문

이 글에서 다루는 키워드 (관련 검색어)

C++, 반복자, iterator, std::begin, std::end, reverse_iterator, iterator_traits, iterator_adapters, std::distance, std::advance 등으로 검색하시면 이 글이 도움이 됩니다.


관련 글

  • C++ 커스텀 반복자 완벽 가이드 | Forward·Bidirectional
  • C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)
  • C++ 패키지 매니저 | vcpkg·Conan으로
  • C++ GDB/LLDB | cout 100개 찍어도 못 찾은 버그, 디버거로 5분 만에 해결
  • C++ 디버깅 기초 완벽 가이드 | GDB·LLDB 브레이크포인트·워치포인트로 버그 5분 만에 찾기