STL이안, Standard Template Library의 약자로, C++ 프로그래밍에 필요한 자료구조와 알고리즘들을 표준으로 구현해 놓은 템플릿 라이브러리다.
STL에는 표준 컨테이너들이 포함되는데, 컨테이너란, 데이터를 저장하는 객체를 의미한다.
STL의 vector는 동적 배열 컨테이너이다. 먼저 배열의 의미를 살펴보자면, 배열이란, 데이터가 연속적으로 저장된 공간을 의미한다. 즉, vector는 동적으로 구현된 배열을 의미한다.
일반적인 배열은 크기가 정해져 있다는 단점이 있다. 이는 초기화시에 정해진 크기 만큼만 데이터를 저장할수 있다는 의미이다.
하지만 vector는 배열과 같이 동작하지만, 크기가 가변적으로 변한다. 즉, 기존의 용량이 가득 차면, vector 내부에서 용량을 증설한다.
vector에는 size와 capacity라는 개념이 존재한다. size는 말 그대로, vector 내부 원소의 개수를 나타낸다. 만약 10개의 요소가 있다면, size = 10인 것이다.
size와 달리 capacity는 vector의 최대 용량을 의미한다. 현재 vector가 담을 수 있는 원소의 최대 개수이다.
capacity는 size와 달리 vector 내부 원소 개수가 감소한다고 해도 줄어들지 않는다. 즉, 한번 증가한 capacity 값은 한번 증가하면 clear()를 통해 vector를 초기화 한다고 해도 감소하지 않는다.
vector는 가변적으로 크기가 변하는 배열이라고 하였는데, 이는 다음의 방법으로 동작한다.
먼저, vector는 초기에 메모리의 여유분을 두고 메모리를 할당한다. 즉, 10개의 원소가 있다면, capacity로 20의 크기의 메모리를 할당하는 방식이다.
만약 vector에 capacity 이상의 원소를 삽입하면 현재 capacity의 특정 배수 (컴파일러에 따라 다르지만, 일반적으로 기존 capacity의 1.5-2배)로 새롭게 메모리 공간을 할당하고, 기존의 데이터를 새롭게 할당한 메모리 공간으로 복사를 진행한다.
vector의 capacity가 증설 될 때 발생하는 데이터 복사로 인해서 오버헤드가 발생하는데, 이는 성능 저하의 원인이 되기도 한다.
capacity 증가시 발생하는 오버헤드를 예방하기 위해서 미리 일정 크기의 capacity를 지정해서 미리 할당하기도 하는데, 이는 reserve()와 resize() 함수를 통해서 이루어진다.
vector의 reserve() 함수는 vector의 capacity를 늘려주는 함수이다. 이는 vector 내부의 데이터에 영향을 미치지 않고, vector가 증설될때 발생하는 동작을 그대로 실행한다.
reserve()를 통해 vector를 증설하면, 해당 크기 만큼 vector의 capacity가 증설된다. reserve()를 통해서는 vector의 증설만 가능하고, capacity의 감소는 불가능하다.
resize()는 reserve()와 달리 capacity만 증가하는게 아니라, 새로운 원소를 추가해서 크기를 늘리는 함수이다.
resize()를 통해서 벡터의 크기를 늘리면 vector 내부의 원소들이 지정한 개수만큼 새로 생성된다. 즉, 기본 생성자로 초기화된 원소들이 새롭게 추가된다는 의미이다. 그렇기 때문에 vector의 size 값이 증가한다.
resize()는 크기의 증가, 감소가 모두 가능하다. 하지만, resize()를 통해 vector의 크기를 감소시키면, 잘려서 소멸되는 데이터가 있을 수 있다.
#include <vector> // vector를 사용하기 위해선 헤더를 include 해줘야한다.
int main(){
// int 타입의 원소들을 가지는 vector 생성
std::vector<int> nums;
// int 타입의 원소들을 가지는 초기 크기가 10인 vector 생성
std::vector<int> vecWsize(10);
// int 타입의 값이 0인 원소 10개를 가진 vector 생성
// 이렇게 vector 생성시에 기본 크기와 값을 지정해 줄수 있다.
std::vector<int> vecWzero(10, 0);
// vector는 배열과 같이 연속적인 메모리 공간에 값을 보관하고 있기 때문에 [] 연산자를 통한 임의 접근 (Random Access)가 가능하다.
int val = vecWzero[0];
}
vector는 처음/끝, 중간에 원소를 삭제/추가 할 수 있다. vector의 중간에 원소를 추가/삭제 하는 작업은 반복자 (iterator)를 통해서 이루어진다.
반복자는 포인터와 유사한 개념으로, 컨테이너 내부의 요소를 가르키는 객체이다.
포인터와 같이 ++, --, +, -등의 연산이 가능하고 *연산자를 통해서 반복자가 가르키는 원소의 값을 얻을 수 있다.
반복자는 하나의 객체로써 컨테이너 내부의 반복자가 가르키는 원소의 주소값을 가진 포인터를 가진다.
컨테이너의 begin() 함수를 통해서 첫 원소를 가르키는 반복자를 획득 할 수 있다. 또한 end() 함수를 통해서 마지막 원소의 다음 주소값을 가르키는 반복자를 획득 할 수 있다.
vector는 배열과 같은 형태의 반복문으로도 순회가 가능하지만, 반복자를 활용한 반복문으로도 순회가 가능하다.
std::vector<int> nums(10, 0);
for(std::vector<int>::iterator iter = nums.begin(); iter != nums.end(); ++iter){
std::cout << (*iter) << std::endl;
}
위와 같은 방법으로 컨테이너 반복자를 통한 원소 순회가 가능하다.
vector의 경우엔 배열과 같이 원소 접근이 가능하기 때문에 위와 같은 반복자를 활용한 순회는 비교적 복잡해 보일 수 있다.
하지만 다른 STL 컨테이너들은 원소들이 연속적인 메모리 공간에 있지 않을수 있기 때문에 반복자를 활용할수도 있다. 따라서 다른 컨테이너와의 통일성을 위해 위와 같이 반복자를 통해 순회하는 경우도 있다.
vector는 처음/끝 원소 말고도 중간에 있는 원소를 삭제 혹은 중간에 삽입이 가능하다. 하지만 이 작업은 중간 원소를 삭제 혹은 삽입 한 후 해당 위치 이후의 원소들을 모두 한칸씩 밀거나 당기는 복사 작업이 이루어지기 떄문에 오버헤드가 발생한다.
또한 vector의 마지막에 원소를 삽입/삭제 말고 제일 앞에 삽입/삭제 하는 경우도 마찬가지로 중간 삽입/삭제와 같은 이유로 오버헤드가 발생한다.
이와 비교해서 맨 끝에 있는 원소의 삽입/삭제는 효율적인 작업이다.
vector<int> v = v(10);
// vector의 4번째 위치에 원소 0을 삽입
v.insert(v.begin() + 4, 0);
for(vector<int>::iterator it = v.begin(); it != v.end();){
int data = *it;
if (data == 3){
it = v.erase(it); // 이렇게 반드시 갱신된 반복자를 받아 주어야함.
// 그리고 새로 갱신된 반복자는 삭제된 원소의 다음 원소값을 가르키기 때문에 한번 더 반복해주어야한다.
}else{
++it; // 이렇게 삭제가 안된 경우에만 증가 해야한다
}
}
위와 같이 erase와 insert를 통해서 vector에 원소를 삽입/삭제 할 수 있는데, 두 함수는 모두 삽입/삭제된 원소를 가르키는 새로운 반복자를 반환한다.
vector 원소의 erase, insert를 진행한 이후에 해당 위치를 가르키는 반복자는 무효화 되기 때문에 반드시 해당 함수이 반환한 반복자로 반복자를 갱신 해 주어야 한다.