본문 바로가기

C++ 일기

21. STL 컨테이너 vector

이번 장에서는 갑자기 모르는 단어뿐으로 제목이 이루어져있다.

STL은 무엇이고, 컨테이너는 무엇이며, 또 vector는 무엇인가?

이를 하나씩 천천히 다루어 보려고 한다.

 

맨 먼저, STL은 무엇인가?

STL은 Standard Template Library의 약자로, 템플릿을 이용한 자료구조, 알고리즘 등을 정리해둔 라이브러리.

즉, 저장소를 표준으로써 지정한 것이다.

우리가 프로그램을 구현하면서, 다루기 복잡한 자료형을 다룰 때도 있고,

구현하기 어렵거나 혹은 구현하는 것이 굉장히 번거로워서

알고리즘이 필요해 사용할 때 마다 오랜 시간을 잡아먹는 알고리즘들이 생길 때가 찾아온다.

그럴 때를 위해, 사용 빈도가 높고, 관리하기 쉬운 자료구조나 알고리즘을 미리 제공하는 것이다.

 

STL에는 대표적으로 3개의 라이브러리가 존재한다.

 

첫 째로는 제목에도 적혀있는 컨테이너다.

컨테이너는 말 그대로 임의의 자료형을 보관하고, 필요에 따라 꺼내서 사용할 수 있는 보관소다.

우리가 여러 자료형을 배열로 선언하여 한번에 관리했던 적이 있지 않은가?

컨테이너도 이와 비슷한 역할을 한다고 말 할 수 있다.

하지만 배열을 관리할 때, 값을 수정하거나 혹은 자료형을 바꿀 필요가 생기거나 하는 등

관리에 어려움을 겪을 수 있는데, 이를 템플릿화 하여 자료형을 간편하게 수정하거나

여러 자료들이 담겨있는 컨테이너들에 쉽게 접근할 수 있도록 여러 기능들을 멤버함수로 탑재시켜둔 보관소가

바로 이 컨테이너다.

 

둘 째로는 반복자(iterator)다.

반복자는 앞서 설명한 컨테이너에 접근하고, 관리하기 쉽도록 설정한 라이브러리이다.

우리가 자료들을 관리할 때 포인터를 사용하여 접근하고, 직접 해당 자료의 값을 바꿀 수 있는데,

라이브러리에서 포인터의 기능을 담당하고 있는 라이브러리가 반복자라고 할 수 있겠다.

반복자는 컨테이너에 접근하여 저장되어있는 자료들에 접근하여 사용자가 편리하게 그 값을 추가하거나

수정할 수 있도록 만들어준다.

 

마지막으로는 알고리즘이다.

알고리즘은 쉽게 말해 그냥 함수다. 

우리가 프로그램을 구현하는데 있어, 혹은 자료형들을 관리함에 있어 자주 사용하거나 유용한 함수들을

정리하여 표준으로 설정한 것이다.

 

위처럼 STL에는 여러 유용한 기능들이 표준으로써 준비되어있기 때문에,

잘만 이용하면 코드를 보다 깔끔하고 가독성이 좋게 짤 수 있으므로,

익숙해지고, 많이 사용해봐야 할 것이다.

그 중에서 이번에는 컨테이너 <vector>에 대해 알아보겠다.

 

 

컨테이너들 중에서도 여러 구조로 되어있는 컨테이너들이 있는데,

vector는 컨테이너 중에서 선형적 구조로 되어있다.

선형적 구조, 즉 직선형태로 되어있다고 이해하면 쉬울 것 같다.

 

vector를 이해하기 위해 우선은 컨테이너를 떠올려보자.

여기서 컨테이너는 위에서 다룬 STL의 라이브러리 중 하나인 컨테이너가 아니라,

일상생활에서 사용되는 컨테이너를 말한다.

내가 주로 떠올리는 컨테이너의 이미지는 게임 서든어택에 있는 맵 중 하나인 웨어하우스에 등장하는 컨테이너이다.

이러한 컨테이너가 자료를 담는 라이브러리인 컨테이너를 이해할 때 머릿속에 그리면 좋은 그림인 것 같다.

 

vector는 컨테이너는 컨테이너인데, 문이 한쪽에 밖에 달려있지 않다.

즉 하나의 문으로 넣고 빼는 모든 행위를 해야한다는 것이다.

그렇기 때문에 vector는 자료를 저장할 때,

먼저 넣은 것이 가장 안 쪽으로, 나중에 넣은 것이 가장 바깥쪽으로 저장된다.

맨 먼저 넣은 것은, 나중에 넣은 것들 속에 

이러한 방식을 선입후출(FILO : First In Last Out)이라고 부른다.

자료구조를 다룰 때 단골로 등장하는 선입후출, 선입선출이라는 말은, 실제로 자료구조를 통해 자료를 다룰 때

잊어선 안되는 중요한 요소이다. 어떠한 자료구조가 어떠한 형식을 가졌는지를 잘 파악해야

필요한 순간에 적절하 자료구조를 통해 편리하게 자료를 관리할 수있기 때문이다.

 

 

그렇다면 이제 vector 컨테이너를 사용해보겠다.

 

#include<iostream>
#include<vector>

int main()
{
   std::vector<int> v;
   v.push_back(10);
   std::cout << v[1];
   //출력값 : 10
}

위와 같이 vector 컨테이너를 사용하기 위해선 전처리기 include를 통해서 vector를 포함시켜줘야 한다.

vector를 가져왔다면, 이제 함수에서 사용해보자.

vector는 std네임스페이스의 멤버이기 때문에, std::로 범위지정연산자를 통해 불러와야 한다.

그리고 앞서 말했듯, STL은 템플릿 라이브러리이기 때문에 템플릿의 typename을 입력해줘야 한다.

여기선 int자료형을 다룰 vector 컨테이너 v를 선언하였다.

선언이 끝나면 이제 vector 컨테이너 v가 만들어 졌다.

이 컨테이너에는 그냥 배열처럼 초기화해서 값을 저장하는 것이 아닌,

vector 컨테이너의 멤버함수인 push_back()을 통해서 값을 집어넣어야 한다.

앞서 vector는 선입후출의 구조라고 설명했었다. 그렇기 때문에 vector에 값을 넣으려면 뒤에서 밀어 넣는 모양이 된다.

 

예를들면, 만원 지하철이라고 해보자.

사람이 가득찬 지하철에서는 먼저 들어온 사람이 나중에 들어온 사람에게 밀리고 밀려 저 구석까지 가게된다.

나중에 들어오는 사람이 뒤에서 밀어서(push_back) 값이 저장되는 것처럼 말이다.

 

이렇게 저장된 데이터는, 접근할 때 배열로 접근해야 한다. 

처음에는 마냥 어렵고 무서웠던 벡터는, 알고보니 그냥 템플릿화 한 배열이었다는 것이다. 갑자기 친숙하다.

 

 

그렇다면, 이렇게 집어넣어진 데이터를 수정하고싶거나 제거하고싶을 때는 어떻게 해야할까?

push_back으로 밀어 넣었으니 당연히 꺼내오는 것도 가능하다.

#include<iostream>
#include<vector>

int main()
{
   std::vector<int> v;
   v.push_back(10);
   v.push_back(20);
   v.push_back(30);
   for(int i = 0; i < v.size(); i++)
   	std::cout << v[i] << std::endl;
    //출력값 10 20 30
   
   v.pop_back();
   for(int i = 0; i < v.size(); i++)
   	std::cout << v[i] << std::endl;
   //출력값 10 20
}

데이터를 빼고싶을 때는 push_back 대신 pop_back을 사용하면 된다.

앞서 말했듯 선입후출이기 때문에 v의 자료를 뺄 때는 나중에 넣은 것 먼저 빠지게 된다.

그래서 위처럼 30이 제일 늦게 넣어졌기에 30이 가장먼저 빠져나왔다.

 

그리고 새롭게 v.size()라는 함수도 볼 수 있는데,

이는 벡터 클래스의 퍼블릭 멤버함수로, 외부에서는 접근할 수 없지만 이 size라는 함수를 통해

내부에 있는 벡터 컨테이너의 크기에 접근해, 벡터의 크기(혹은 길이)를 알 수 있다.

 

 

벡터 컨테이너를 포함한 선입후출 구조의 자료구조는 마치 총기의 탄알집 같다.

장전을 할 때는 위에서부터 밀어 넣으며, 격발할 때는 맨 나중에 넣은 것이 가장 먼저 격발된다.

탄알집에 총알을 장전하는 것이 push_back, 격발하는 것이 pop_back의 이미지와 굉장히 유사해서

이해하기 쉬웠다.

 

벡터는 이러한 구조를 가지고있기 때문에 자료의 중간 삽입이나 삭제, 수정등이 빈번한 자료형을 다룰 때 굉장히 불리하다.

예를 들어 벡터 안에 10,000개의 자료가 저장되어있다고 가정해보자.

이 때, 5000번째에 있는 자료를 삭제하려고 한다면, 5000번째에 있는 자료가 삭제되는 순간,

5001번째 자료부터 10000번째 자료까지 전부 한칸씩 안쪽으로 들어가게 된다. 

그러면 여태껏 저장했던 자료구조의 인덱스가 전부 달라지게 되어 다시 접근하려고 할 때 곤란해진다.

 

 

컨테이너에 대해 배웠으니 반복자 (iterator)에 대해서 간략하게 알아보겠다.

반복자는 쉽게말해 레이저 포인터 같은 존재다.

컨테이너는 선형적 구조라고 설명한 적이 있었다.

일직선상으로 길게 늘어진 컨테이너들의 원소는 맨 처음 원소와 마지막 원소를 제외하고는

모두 양 옆의 공간에 빈 곳 없이 연결되어있어야 한다.

 

 

 

 

반복자는 이러한 원소들이 어디있는지 그 주소를 알려주며,

포인터이기 때문에 물론 역추적을 통해 값에 접근하는 것도 가능하다.

#include<iostream>

int main()
{
   std::vector<int> v1;
	v1.push_back(10);
	v1.push_back(10);
	v1.push_back(10);
	v1.push_back(10);
	v1.push_back(10);

	std::vector<int>::iterator iter;
	for (iter = v1.begin(); iter != v1.end(); ++iter)
	    std::cout << *iter << std::endl;
    // 출력값 : 10 10 10 10 10 
}

 

여기서 역시 begin과 end라는 함수가 처음으로 등장했다.

begin은 말 뜻대로 해당 벡터의 맨 처음 자료의 위치를 알려준다. 

end 역시 말 뜻 그대로 해당 벡터의 마지막 자료의 위치를 알려준다.

따라서 위의 for문 대로 반복시, iter가 가리키는 주소는 벡터의 첫 자료부터 시작해서

순서대로 마지막 자료에 도달할 때 까지 모든 자료를 가리킨다.

이는 모든 자료구조에 해당되는 것은 아니고, 벡터가 선형구조를 가지고있기 때문에 그렇다.

또한, 이렇게 가리킨 자료에 모두 역추적을 통해 값을 가져오고, 

이를 std::cout을 통해 출력했다.

 

이처럼 자료들의 주소를 가리키는 역할 뿐만 아니라, 각 원소들을 연결해주는 역할도 한다.

자료의 주소를 가리키기만 하는 레이저포인터가 어떻게 원소들을 연결해주는 것일까?

사실 이 레이저포인터는 빛이 하나로 나오는 것이 아니라 여러 갈래로 나누어진다.

이렇게 여러갈래로 나누어진 빛은 자료의 위치 뿐만 아니라, 양 옆에 무슨 원소가 있는지 알려주는 이정표를 비춘다.

따라서 반복자가 다른 이정표를 비춰주면, 그 자료가 위치해있는 자리가 바뀌며 정렬된다.

위에서, 중간에 있는 자료가 빠지게 되었을 때 벡터는 뒤의 모든 자료를 한칸씩 옮긴다고 했었다.

이 역할을 반복자가 수행한다. 모든 자료에 접근하여 5001번째 자료의 이정표가 다음 자료로 5002번째 자료를,

이전 자료로 5000번째 자료를 가리키고 있었는데,

이중 5000번째 자료를 가리키는 이정표를 비추지 않고, 4999번째 자료를 가리키는 이정표를 비춤으로써,

자리를 옮겨주는 것이다.

 

 

 

'C++ 일기' 카테고리의 다른 글

20. 템플릿  (0) 2024.02.29
19. 클래스(Class)  (2) 2024.02.28
18. 객체지향(OOP)과 절차지향(PP)프로그래밍  (2) 2024.02.28
17. 네임스페이스 (namespace)  (0) 2024.02.28
16. Call by Reference, Call by Value, Call by Reference  (0) 2024.02.27