본문 바로가기

개발일지

C++ 표준 템플릿 라이브러리 (Standard Template Library - STL)

C++ 에서는 개발자 편의를 위하여 표준 템플릿 라이브러리를 제공한다.

 

STL는 하기처럼 3개의  라이브러리로 구성되어있습니다.

  • 임의 타입의 객체를 보관할 수 있는 컨테이너 (container)
  • 컨테이너에 보관된 원소에 접근할 수 있는 반복자 (iterator)
  • 반복자들을 가지고 일련의 작업을 수행하는 알고리즘 (algorithm)
  •  

C++ `STL` 컨테이너 - 벡터 (std::vector)

먼저 시퀀스 컨테이너의 경우 vector, list, deque 이렇게 3 개가 정의되어 있습니다. 

vector의 경우 가변길이 배열로써 템플릿에 사전 정의된 함수(push_back)로 원소를 추가하거나 해당 원소에 접근([], at함수) 할 수 있습니다.

 

그럼 아래에 간단한 예제를 들어보겠습니다.

 

#include <iostream>
#include <vector>

using namespace std;

int main() {
  vector<int> vec;
  vec.push_back(10);  // 맨 뒤에 10 추가
  vec.push_back(20);  // 맨 뒤에 20 추가
  vec.push_back(30);  // 맨 뒤에 30 추가
  vec.push_back(40);  // 맨 뒤에 40 추가

  for (vector<int>::size_type i = 0; i < vec.size(); i++) {
    cout << "vec 의 " << i + 1 << " 번째 원소 :: " << vec[i] <<endl;
  }
}

 

출력값

 

다음으로 반복자 (iterator) 는  원소에 접근할 수 있는 포인터와 같은 객체입니다.

물론 벡터의 경우 [ ] 를 이용해서 정수형 변수로 마치 배열 처럼 임의의 위치에 접근할 수 있지만, 반복자를 사용해서도 마찬가지 작업을 수행할 수 있습니다.

vector 의 경우 반복자를 얻기 위해서는 begin() 함수와 end() 함수를 사용할 수 있는데 이는 다음과 같은 위치를 리턴합니다.

 

begin() 함수는 예상했던 대로, vector 의 첫번째 원소를 가리키는 반복자를 리턴합니다. 그런데, 흥미롭게도 end() 의 경우 vector 의 마지막 원소 한 칸 뒤를 가리키는 반복자를 리턴하게 됩니다.

리스트 메모리 구조

 

vector.begin() 및 .end() 함수는 vector 의 주소값을 반환한다.

 

 

// 반복자 사용 예시
#include <iostream>
#include <vector>

using namespace std;

int main() {
  vector<int> vec;
  vec.push_back(10);
  vec.push_back(20);
  vec.push_back(30);
  vec.push_back(40);

  // 전체 벡터를 출력하기
  for (vector<int>::iterator itr = vec.begin(); itr != vec.end(); itr++) {
    cout << *itr << endl;
  }
 

  vector<int>::iterator itr = vec.begin() + 2;
  cout << "3 번째 원소 :: " << *itr << endl;
}

vec.end()를 활용

 

 

반복자를 이용하면 아래와 같이 .insert()  .erase() 함수도 사용할 수 있습니다.

insert(주소값 , 원소) 함수를 사용하면 해당 주소 앞에 원소를 추가할 수 있습니다.

erase(주소값 ) 함수는 해당 주소값에 있는 원소를 삭제합니다.

 

 

#include <iostream>
#include <vector>

using namespace std;


template <typename T>
void print_vector(vector<T>& vec) {
  // 전체 벡터를 출력하기
  for (typename vector<T>::iterator itr = vec.begin(); itr != vec.end(); itr++) {
    cout << *itr << endl;
  }
}
int main() {
  vector<int> vec;
  vec.push_back(10);
  vec.push_back(20);
  vec.push_back(30);
  vec.push_back(40);

  cout << "처음 벡터 상태" << endl;
  print_vector(vec);
  cout << "----------------------------" << endl;

  // vec[2] 앞에 15 추가
  vec.insert(vec.begin() + 2, 15);
  print_vector(vec);

  cout << "----------------------------" << endl;
  // vec[3] 제거
  vec.erase(vec.begin() + 3);
  print_vector(vec);
}
 

 

리스트 (list)

다음은 리스트(list) 입니다.

리스트(list) 의 경우 양방향 연결 구조를 가진 자료형이라 볼 수 있습니다.

따라서 vector 와는 달리 임의의 위치에 있는 원소에 접근을 바로 할 수 없습니다. 

list 컨테이너 자체에서는 시작 원소와 마지막 원소의 위치만을 기억하기 때문에, 임의의 위치에 있는 원소에 접근하기 위해서는 하나씩 링크를 따라가야 합니다.

 

그래서 리스트에는 아예 []  at 함수가 아예 정의되어 있지 않습니다.

 

#include <iostream>
#include <list>

using namespace std;

int main() {
  list<int> lst;

  lst.push_back(10);
  lst.push_back(20);
  lst.push_back(30);
  lst.push_back(40);

  for (list<int>::iterator itr = lst.begin(); itr != lst.end(); itr++){
    cout << *itr << endl;
  }
}
 
리스트

 

리스트의 반복자는 한칸씩만 움직일 수 있습니다.

이와 같은 이유는 list 의 구조를 생각해보면 알 수 있습니다. 앞서 말했듯이 리스트는 왼쪽 혹은 오른쪽을 가리키고 있는 원소들의 모임으로 이루어져 있기 때문에, 한 번에 한 칸 씩 밖에 이동할 수 없습니다.

 

list에도 vector와 마찬가지로 erase() 함수 및 insert() 가 가능합니다.

 

 

 

덱 (deque - double ended queue)

마지막으로는 덱(deque) 자료형 입니다.덱은 벡터와 비슷하게 임의의 위치의 원소에 접근할 수 있으며 맨 뒤에 원소를 추가/제거 하는 작업도 수행할 수 있습니다. 

덱의 경우 원소들이 실제로 메모리 상에서 연속적으로 존재하지는 않습니다. 이 때문에 원소들이 어디에 저장되어 있는지에 대한 정보를 보관하기 위해 추가적인 메모리가 더 필요로 합니다.

덱 메모리 구조

 

위 그림은 덱이 어떠한 구조를 가지는지 보여줍니다. 일단, 벡터와는 다르게 원소들이 메모리에 연속되어 존재하는 것이 아니라 일정 크기로 잘려서 각각의 블록 속에 존재합니다. 따라서 이 블록들이 메모리 상에 어느 곳에 위치하여 있는지 저장하기 위해서 각각의 블록들의 주소를 저장하는 벡터가 필요로 합니다.

 

 

#include <deque>
#include <iostream>

 

using namespace std;

 

template <typename T>
void print_deque(std::deque<T>& dq) {
  // 전체 덱을 출력하기
  std::cout << "[ ";
  for (const auto& elem : dq) {
    std::cout << elem << " ";
  }
  std::cout << " ] " << std::endl;
}
int main() {
  std::deque<int> dq;
  dq.push_back(10);
  dq.push_back(20);
  dq.push_front(30);
  dq.push_front(40);

 

  std::cout << "초기 dq 상태" << std::endl;
  print_deque(dq);

 

  std::cout << "맨 앞의 원소 제거" << std::endl;
  dq.pop_front();
  print_deque(dq);
}

 

벡터의 경우 push_back() 으로 제일 뒤에 원소를 추가할 수 있으며, push_front() 로 가장앞에 원소를 추가할 수도 있습니다.

그리고 .pop_front() 의 경우 맨앞의 원소를 제거할 수도 있습니다.

덱 역시 벡터 처럼 임의의 위치에 원소에 접근할 수 있으므로 []  at 함수를 제공합니다.