강의 보고 공부한 것을 정리하는 목적으로 작성된 글이므로 틀린 점이 있을 수 있음에 양해 부탁드립니다. (피드백 환영입니다)
일단 왜 필요한지에 대해 알아보겠다.
일단 대표적으로 vector
를 이용해 순회를 하고 출력하는 코드가
vector<int> v{1, 2, 3, 4, 5};
for(int i = 0; i < v.size(); i++)
{
int data = v[i];
cout << data << endl;
}
for(auto it = v.begin(); it != v.end(); it++)
{
int data = *it;
cout << data << endl;
}
이런식으로 있다고 했을 때
위와 같은 코드처럼 모든 것을 스캔하면 추가/삭제
를 하지 않는다면 굳이 저렇게 힘들게 적을 필요가 없다.
그래서 세 번째 문법이 등장하는데 그것이 range-based for
이다.
for(int data : v)
{
cout << data << endl;
}
이런식으로 사용하는데 정말로 간단하게 사용할 수 있다.
C#의 foreach를 생각하면 된다.
보면 가독성 측면으로도 굉장히 알아보기도 쉽고 코드를 작성할 때도 편리하다.
하지만 속도는 진짜 조금 느리다고 한다.
그럼에도 1,2번 코드보다 훨씬 좋은 코드라고 볼 수 있다.
🍀사용하면 좋다!
우리가 데이터를 바꾸고 싶은 때가 있을 수 있다.
vector<int> v{1, 2, 3, 4, 5};
for(int i = 0; i < v.size(); i++)
{
v[i] = 100;
cout << data << endl;
}
for(auto it = v.begin(); it != v.end(); it++)
{
*it = 100;
cout << data << endl;
}
이런식으로 할 수 있었다.
그럼 range-based for
로는 어떤식으로 사용 할 수 있을까?
for(int& data : v)
{
data = 100;
cout << data << endl;
}
바로 reference(&)
를 붙여서 작업을 해주면 된다.
수정하는게 안되는 것이 아니라 추가/삭제
가 안되는 것이기 때문에 하다가 바꾸고 싶은 것이 있다면 &를 붙여서 작업하면 된다.
하지만 &를 붙이는 경우는 int,float처럼 데이터가 작은 경우가 아니라
struct itemInfo
{
// 엄청 큰 데이터들
};
int main()
{
vector<itemInfo> v;
for (auto& data : v)
{
}
}
이러한 것과 같이 엄청 큰 데이터들을 가지고 있을 때
는 복사가 크게 일어나기 때문에 &
를 붙여줘야 한다.
그래서 보통 위처럼 사용한다.
그러면 여기서 range-based for
가 얼마나 느리고, 어떤식으로 동작하는지 궁금할 수 있다. 그래서 알아볼 것이다.
우리는 커스텀 클래스를 사용하여 구현을 해볼 것이다.
예를 들어보면
class Inventory
{
public:
int _items[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
};
int main()
{
Inventory inventory;
}
이런식으로 있을 때 inventory의 _items를 순회하고 싶다고 하면
class Inventory
{
public:
int _items[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
};
int main()
{
Inventory inventory;
for(auto item : inventory)
{
}
}
이런식으로 짜줄텐데 이런식으로 하면 오류가 뜰 것이다.
즉, 두 번째 방식으로 코드가 만들어진다고 볼 수 있다.
for(auto it = v.begin(); it != v.end(); it++)
{
*it = 100;
cout << data << endl;
}
이 방식
그래서 저번에 우리가 iterator
를 구현해본 것처럼
v.begin(), v.end(), it++, !=, *it
이러한 것들을 클래스에 구현을 해준다면 정상적으로 작동할 것이라는 것을 알 수 있다.
그럼 다시 iterator
를 만들어보자!
class Iterator
{
public:
Iterator() : _data(nullptr) { }
Iterator(int* data) : _data(data) { }
bool operator==(const Iterator& other)
{
return _data == other._data;
}
bool operator!=(const Iterator& other)
{
return _data != other._data;
}
void operator++()
{
_data++;
}
int& operator*()
{
return *_data;
}
public:
int* _data;
};
class Inventory
{
public:
using iterator = Iterator;
iterator begin() { return iteraotr(&_items[0]); }
iterator end() { return iterator(&_items[10]); }
int _items[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
};
이렇게 만들어진다면
뿅! 이런식으로 오류가 사라진 것을 볼 수 있다.
그럼이제 총 코드를
#include <iostream>
#include <vector>
using namespace std;
class Iterator
{
public:
Iterator() : _data(nullptr) {}
Iterator(int* data) : _data(data) {}
bool operator==(const Iterator& other)
{
return _data == other._data;
}
bool operator!=(const Iterator& other)
{
return _data != other._data;
}
void operator++()
{
_data++;
}
int operator*()
{
return *_data;
}
public:
int* _data;
};
class Inventory
{
public:
using iterator = Iterator;
iterator begin() { return iterator(&_items[0]); }
iterator end() { return iterator(&_items[10]); }
int _items[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
};
int main()
{
Inventory inventory;
for (auto item : inventory)
{
cout << item << endl;
}
}
이런식으로 짜준다면
이런식으로 잘 출력이 되는 걸 알 수 있다!
그래서 range-based for라는 것은 우리 눈에는 :로 보이지만 내부적으로는
for(auto it = v.begin(); it != v.end(); it++)
{
*it = 100;
cout << data << endl;
}
이 방식과 같다는 사실을 알 수 있다!
뭐 사실 우리가 iterator
를 직접만드는 일은 발생하지 않을 것이고
어지간해서는 container를 사용하여서 코드를 짤 것이기 때문에 편리하게 사용할 수 있게 될 것이다.
그래서 앞으로 어떤 container를 사용하여서 순회를 해서 무언가를 해야한다? 라고 하면 range-based for
를 떠올리면 된다.
특이한 경우면 &도 붙여주고
그래서 auto
와 range-based for
는 배워야지만 엄청나게 코딩할 때 편해진다.
물론 실시간으로 추가/삭제
를 하면 안된다.
데이터를 수정하는 것은 상관이 없지만 이 데이터를 안에서 빼거나 넣거나 한다면 문제가 될 것이다.
추가/삭제
가 필요하면 원래방식으로 돌아오거나 나중에 배우는 알고리즘을 사용해서 할 수도 있다고 한다.
뭐가 되었든 range-based for
에서는 데이터를 추가/삭제
할 생각을 하면 안된다.
메모리 오염이 생길 수 있다.
근데 생각보다 사람들이 이러한 실수를 많이 한다고 한다.
왜냐하면 거기서 어떤 함수를 들어가서 지우는 작업을 해도 메모리 오염이나 다른 오류들이 일어날 수 있기 때문이다.
그래서 오늘은 range-based for
를 배웠는데
range-based for
는 정말 편리하게 사용할 수 있는 문법이기 때문에 꼭 알고 있느 것이 좋고
조심해야 할 점은 전체 스캔을 할 때는 정말 간단하게 필요한 작업만 골라서 해야한다.
다음에는 map을 배우기 위해 우선순위 큐와 양대산맥을 이루는 탐색 트리(이진 트리)를 배워볼 것이다.