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
목차
- 문제 시나리오와 원인 분석
- 반복자 카테고리
- begin과 end 완전 가이드
- 역방향 반복자 (reverse_iterator)
- 반복자 어댑터
- std::distance와 std::advance
- 완전한 반복자 예제 모음
- 커스텀 반복자 구현
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
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_iterator를 erase에 넘기려 할 때 잘못된 원소가 삭제됨.
원인: 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 == it2 | istream_iterator |
| Output | *it = x, ++it | ostream_iterator, back_inserter |
| Forward | Input + 다회 순회 | forward_list, unordered_* |
| Bidirectional | Forward + --it | list, map, set |
| Random Access | Bidirectional + 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/cend는 const_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) 직전까지 순회합니다. range가 std::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 = value 시 dst.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_inserter는 push_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 = value 시 stream << 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::advance와 std::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)은 it를 n칸 이동합니다. 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). first가 last보다 뒤면 미정의 동작입니다.
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 제거
위 코드 설명: find에 rbegin/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:
vector는push_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에 넘기지 않는가? -
list에std::sort대신std::list::sort사용하는가? - 반복자 저장 후
push_back/insert시 재획득하는가?
실무 팁
개발 시 주의사항
-
[팁 1]: [설명]
// 예시 코드 -
[팁 2]: [설명]
// 예시 코드 -
[팁 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 사용법, 빈 컨테이너 처리.
참고 자료
- cppreference - Iterator
- cppreference - Iterator library
- C++ vector 기초 | 초기화·연산·용량 관리와 실전 패턴
- C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기
- C++ 범위 기반 for 루프
한 줄 요약: 반복자는 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분 만에 찾기