안녕하세요
지난 글에는 C++의 컨테이너와 이터레이터가 무엇인지, 어떻게 사용하는지에 대해 알아보았습니다. 이번에는 조금 컨테이너와 이터레이터가 내부적으로 어떻게 동작하는 알아보겠습니다.
1. 컨테이너에 [] 연산자 사용
std::vector를 사용할 때, 우리는 배열처럼 v[i] 형태로 특정 위치의 요소에 매우 편리하게 접근할 수 있습니다.
이유는 연산자 오버로딩(Operator Overloading) 때문입니다. []는 C++ 클래스에서 특별한 함수(멤버 함수)로 직접 구현할 수 있는 연산자 중 하나입니다.
원리를 확인하기 위해, std::vector를 아주 간단하게 흉내 낸 ThingVector라는 클래스를 만들어 보겠습니다.
template<typename T>
class ThingVector {
private:
T data[100]; // 설명을 위해 고정 크기 배열 사용
int iBegin;
int iEnd;
public:
ThingVector() : iBegin(0), iEnd(0) {}
// ... (다른 함수들) ...
// [] 연산자를 멤버 함수로 구현 (오버로딩)
T& operator[](int index) {
return data[index];
}
};
위 코드에서 operator [] 함수 사용해서 v[5]와 같은 코드를 사용할 수 있습니다. 이 코드는 내부적으로 v.operator[](5)를 호출하는 것과 같습니다.
왜 반환 타입이 T가 아니라 T&(참조, Reference)을 한 것은 만약 T로 반환하면 값을 읽을 수만 있습니다 (int a = v[0];).
하지만 T&로 반환하면 해당 메모리 공간 자체를 가리키게 되므로, 값을 읽는 것은 물론 값을 수정하는 것도 가능합니다. (v[0] = 100;).
2. 이터레이터(Iterator) 기능 추가
모든 컨테이너가 [] 연산자를 지원하지는 않습니다. 예를 들어, std::list는 요소들이 메모리에 연속적으로 저장되어 있지 않기 때문에, i번째 요소에 한 번에 접근하는 것이 비효율적입니다.
그래서 이터레이터라는 통일된 접근 방식이 필요합니다. 이제 ThingVector에 이터레이터 기능을 추가해 보겠습니다.
이터레이터의 실제 구현은 복잡한 구조체(struct)이지만, 그 핵심 개념은 "컨테이너 속 요소를 가리키는 포인터"입니다. ThingVector는 내부적으로 배열을 사용하므로, 이 개념을 실제 포인터를 사용하여 간단하게 구현해 볼 수 있습니다.
template<typename T>
class ThingVector {
public:
// 외부에서 ThingVector<int>::iterator 처럼 접근해야 하므로 public에 둡니다.
typedef T* iterator;
private:
T data[100];
int iBegin;
int iEnd;
public:
// ... (생성자, operator[] 등) ...
iterator begin() {
return &data[iBegin];
}
iterator end() {
return &data[iEnd];
}
};
코드를 간략하게 보자면,
1) typedef T* iterator;
ThingVector의 이터레이터는 "T 타입 데이터의 포인터(T*)"와 같다고 iterator라는 별명을 붙여준 것입니다. 이렇게 하면 ThingVector<int>::iterator는 int*와 같은 타입이 됩니다.
2) iterator begin()
컨테이너의 첫 번째 요소(data[0])의 메모리 주소를 반환합니다.
3) iterator end()
컨테이너의 마지막 요소 바로 다음(data[iEnd])의 메모리 주소를 반환합니다.
3. 전체 코드를 통해 동작 확인
ThingVector 클래스 사용하는 전체 예제 코드를 통해 우리가 만든 [] 연산자와 이터레이터가 어떻게 동작하는지 확인해 봅시다.
#include <iostream>
// #include <vector>
template<typename T>
class ThingVector {
public:
// 이터레이터 타입을 T에 대한 포인터로 정의
typedef T* iterator;
private:
T data[100]; // 간단한 설명을 위해 고정 크기 배열 사용
int iBegin;
int iEnd;
public:
ThingVector() {
iBegin = 0;
iEnd = 0;
}
void push_back(T _data) {
data[iEnd] = _data;
iEnd += 1;
}
// [] 연산자 오버로딩. T&를 반환하여 읽기/쓰기 모두 가능하게 함
T& operator[](int index) {
return data[index];
}
int size() const {
return iEnd - iBegin;
}
iterator begin() {
return &data[iBegin];
}
iterator end() {
return &data[iEnd];
}
};
int main()
{
// std::vector 대신 만든 ThingVector 사용
ThingVector<int> v;
v.push_back(4);
v.push_back(7);
v.push_back(9);
v.push_back(1);
v.push_back(3);
// 방법 1: [] 연산자로 요소 접근
printf("--- Access with [] operator ---\n");
for (int i = 0; i < v.size(); i++)
{
printf("%d\n", v[i]); // 내부적으로 v.operator[](i) 호출
}
printf("------------\n");
// 방법 2: 이터레이터로 요소 접근 (어떤 컨테이너든 일관된 방식)
printf("--- Access with iterator ---\n");
ThingVector<int>::iterator it;
for (it = v.begin(); it != v.end(); ++it)
{
// it는 포인터이므로, *it를 통해 실제 값을 가져옴
const int iNumber = *it;
printf("%d\n", iNumber);
}
return 0;
}
< 실행 결과 >
[] 연산자와, iterator 타입을 이용해서도 잘 작동하는 것을 확인할 수 있습니다.
4. 정리해 보기
- [] 접근은 연산자 오버로딩이라는 C++ 문법을 통해 구현가능합니다.
- 이터레이터(iterator)는 포인터와 유사하게 동작하며, begin()과 end() 함수를 통해 컨테이너의 모든 요소를 일관된 방식으로 순회할 수 있게 해 줍니다. (컨테이너와 이터레이터는 C++ STL의 핵심요소 중 하나)
위의 내용으로 원리에 대해서 알아보았지만, 실제 std::vector와 같은 STL 컨테이너들은 메모리 관리, 예외 처리 등 훨씬 더 효율적으로 구현되어 있습니다.
원리를 조금 알고 사용하면 더 좋지 않을까 합니다.
감사합니다.
<참고 자료>
1. C++ Understanding: 고급 Advanced
https://www.youtube.com/playlist?list=PLrrTotxaO6khn83BjtBN-1HMDc9MZ__yt
2. Effective Modern 이펙티브 모던 C++, Book
'Programming > C, C++' 카테고리의 다른 글
[C++] shared_ptr (1) - 필요성에 대해 알아보기 (0) | 2025.06.20 |
---|---|
[C++] Thread Local Storage (TLS)인 thread_local 대해 알아보기 (4) | 2025.06.12 |
[C++] 컨테이너(Container)와 이터레이터(Iterator) 기초 1 (10) | 2025.05.22 |
[C++] 타입 캐스팅 (static_cast, dynamic_cast) 대하여 (0) | 2025.05.10 |
[C++] 함수 템플릿 기초와 Name Mangling (Name Decoration) 대해 알아보기 (0) | 2025.05.03 |