본문 바로가기

Software/C/C++

[C++. STL] 1.1 표준 C++ 라이브러리(standard C++ library)란?

1.1 표준 C++ 라이브러리(standard C++ library)란?

 
국제 표준 기구(International Standards Organization, ISO)와 미국 국가 표준 기관(American National Standards Institute, ANSI)은 C++ 프로그래밍 언어의 표준화 작업을 마쳤다(표준 번호: ISO/IEC 14882). 이 표준화 과정에서 가장 중요한 부분의 하나가 바로 「표준 C++ 라이브러리(standard C++ library)」이며, 이 라이브러리는 많은 양의 클래스와 함수들을 제공하고 있다.
ANSI/ISO 표준 C++ 라이브러리는 다음을 포함하고 있다.
  • 많은 양의 데이터 구조와 알고리듬. 특히 이 부분만 따로 「표준 템플릿 라이브러리(standard template library, STL)」라고 부른다.
  • 입출력 스트림
  • locale 기능
  • string 템플릿 클래스
  • complex 템플릿 클래스
  • numeric_limits 템플릿 클래스
  • 메모리 관리 기능
  • Language support 기능
  • 예외 처리(exception handling) 기능
  • 수치 배열용으로 최적화된 valarray 클래스
 

1.2 표준 C++ 라이브러리(Standard C++ Library)와 다른 라이브러리와의 차이점

 
표준 C++ 라이브러리는 표준 데이터 구조에 대한 클래스 정의와 이러한 데이터 구조를 다룰 때 주로 사용되는 알고리듬들이 대부분을 차지하고 있다. 이러한 클래스 정의와 알고리듬들을 STL(Standard Template Library)이라고 한다. STL의 구조와 설계는 대부분의 다른 C++ 라이브러리와는 거의 모든 면에서 완전히 다르다. 실례로, STL은 캡슐화(encapsulation)를 피하고 있고, 상속(inheritance)을 거의 사용하고 있지 않다.
캡슐화(encapsulation)는 객체지향 프로그래밍의 트레이드 마크에 해당한다. 데이터와 함수를 객체로 묶는다는 개념은 소프트웨어 개발에서의 가장 강력한 원리이며, 실제로 가장 주된 테크닉이다. encapsulation을 적절히 잘만 사용하면, 지나치게 복잡한 소프트웨어 시스템도 다루기 적절한 크기로 나누어서 개발팀에 속한 각각의 프로그래머에게 할당할 수 있다.
상속(inheritance)은 코드 공유와 소프트웨어 재사용을 가능케하는 강력한 기법이다. 하지만, 예를 들어, GUI에서 두가지 형태의 윈도우는 하나의 공통된 베이스 윈도우 클래스로부터 상속될 수 있고, 각각의 서브 클래스는 각기 필요한 자기만의 특징을 제공할 수 있다. 또 다른 예로, 객체지향 컨테이너 클래스는 공통된 behavior를 보장하며, 더 일반적인 클래스로부터 상속하고, 공통된 멤버 함수를 추출함으로써, 코드 재사용을 지원할 수 있다.
STL의 설계자는 객체지향 방법을 피했으며, 공통된 데이터 구조를 사용하여 수행하는 작업들을 데이터구조의 표현과 분리하였다. 따라서, STL을 '알고리듬의 집합'과 이들 알고리듬을 사용하여 다루는 '데이터 구조들의 집합'으로 보는 것이 적절하다고 하겠다.
 
 
 
 
 
 
 
1.3 비객체지향설계의 장단점
 
표준 C++ 라이브러리의 STL(Standard Template Library) 부분은 설계시 일부러 객체지향 방식을 따르지 않았다. 개발자는 비객체지향적으로 설계된 STL의 장점과 단점들을 잘 파악함으로써, 라이브러리를 효과적으로 사용할 수 있을 것이다. 이들 중 몇가지를 살펴보자.
    소스 코드 크기의 축소
    STL에는 약 50여개의 다양한 알고리듬과 10여개의 주요 데이터 구조들이 들어 있다. 이렇게 알고리듬과 데이터 구조를 분리함으로써, 소스코드의 크기를 줄이고, 유사한 형태의 작업들이 상이한 형태의 인터페이스를 가지게 될 위험을 줄이는 효과를 가져온다. 만약 이렇게 분리를 하지 않는다면, 서로 다른 데이터 구조 각각에 대해 모든 알고리듬들을 또 구현해야 하며, 결국 수 백개의 멤버함수를 필요로하게 될 것이다.
    유연성
    알고리듬과 데이터를 분리함으로써 얻게 되는 장점은 STL의 알고리듬들이 기존의 C++ 포인터와 배열에도 사용할 수 있다는 것이다. C++ 배열은 객체가 아니기 때문에, 클래스 계층구조내에 encapsulate 되어 있는 알고리듬은 이러한 기능을 가질 수 없다.
    효율성
    일반적으로 표준 C++ 라이브러리는 그리고, 특히 STL은 C++ 응용 프로그램을 개발하는데 대한 저수준 접근법을 제공한다. 이와 같은 저수준 접근법은 특정 프로그램이 효율적인 코딩과 수행속도에 중점을 두고 있을 때 유용하다.
    반복자(iterator): 불일치(mismatch)와 무효화(invalidation)
    표준 C++ 라이브러리 데이터 구조는 반복자라 불리는 포인터와 비슷한 객체를 사용하여 컨테이너의 내용물을 가리킨다(2장에서 자세히 설명). 이 라이브러리의 아키텍쳐에서는 이들 반복자들이 같은 컨테이너로부터 온것인지를 증명하기가 불가능하다. 고의든 우연이든 한 컨테이너의 시작 반복자를 다른 컨테이너의 끝 반복자와 같이 사용하게 되면 어떤 일이 일어나게 될 지 장담할 수 없게 된다.
    그리고, 반복자는 자신과 연관된 컨테이너에 대해 삽입이나 반복을 수행한 뒤에, 무효화 될 수도 있다는 사실을 프로그래머는 반드시 기억하고 있어야 한다. 이렇게 무효화된 반복자를 검사도 않고 사용하게 되면 예상치 못한 결과를 초래하게 된다.
    표준 C++ 라이브러리에 익숙해져야 반복자와 관련된 에러의 수를 줄일 수 있다.
    템플릿: 에러와 코드 확대('code bloat')
    템플릿 알고리듬을 사용함으로써 유연성과 강점을 얻게 되지만, 다른 한편으로 디버깅이 힘들어진다. generic 알고리듬의 인자 리스트에서의 에러는 템플릿 확장 과정에서 깊숙히 정의되어 있는 내부함수에 대한 컴파일러 에러를 유발하게 되어 매우 이해하기가 힘들어지게 된다. 알고리듬이 요구하는 바를 잘 이해하여야, 표준 라이브러리를 성공적으로 사용할 수 있다.
    STL은 템플릿에 크게 의존하고 있기 때문에, STL을 사용한 프로그램은 생각했던 것보다 덩치가 훨씬 커질 수 있다.(보통 'code bloat'라 지칭) 특정 템플릿 클래스를 개체화하는데 드는 비용을 인식하여 적절한 결정을 해야 이러한 문제를 최소화할 수 있다(역시 어려운 문제). 컴파일러가 템플릿을 처리하는데 있어 좀더 똑똑해진다면, 이러한 문제들을 줄일 수 있을 것이다.
    문제점: 다중 쓰레딩
    다중 쓰레드 환경하에서 표준 C++ 라이브러리를 사용할 때는 조심해야 한다. 반복자는 컨테이너와는 독립적으로 존재하기 때문에 쓰레드들간에 안전하게 전달할 수가 없다.
     
     
     
     

    1.4 STL의 구조

    소프트웨어를 구성하는 요소들을 3차원 좌표계로 표시한다면 아래 그림과 같이 나타낼 수 있을 것이다. 첫번째 차원은 데이터 타입(int, double 등등)을 나타내며, 두번째 차원은 서로 다른 컨테이너(vector, list 등등)를 나타내며, 세번째 차원은 컨테이너에 대한 여러 알고리듬(검색, 정렬,삭제 등등)을 나타낸다. 각 차원의 크기가 각각 i, j, k라고 한다면, i*j*k 가지의 코드를 모두 작성해야 한다. 데이터 타입을 인자로 사용하는 템플릿 기능을 사용하면, j*k 가지로 줄일 수 있다. 더 나아가서, 알고리듬을 여러가지 컨테이너에 두루 사용되도록 만든다면, j+k 가지의 코드를 생성하면 된다. 예를 들어, double, int, string 타입의 원소를 담고 있는 vector, list, set 컨테이너에 대해 정렬, 검색, 삭제 알고리듬을 만들려면, 총 27가지의 알고리듬을 만들어야 하지만, 아래 그림과 같이 데이터 타입, 컨테이너, 알고리듬을 모두 분리함으로써, 3가지의 컨테이너와 3가지의 알고리듬만을 고안하면 되는 것이다. 아래 그림의 파란 점은 '정수' 'list'를 '정렬'하는 알고리듬을 나타내고 있다.
    STL의 구조
    라이브러리를 이와 같은 구조로 만든다면 소프트웨어 설계 작업량을 상당히 줄일 수 있다. 뿐만 아니라 라이브러리가 제공하는 요소들을 사용자가 만든 요소들과 같이 사용하는 것이 아주 자연스러워진다. 다시 말해서, 라이브러리에서 제공하는 알고리듬을 사용자가 정의한 컨테이너에 사용할 수 있으며, 사용자가 만든 알고리듬을 라이브러리가 기본적으로 제공하는 여러 컨테이너에 적용할 수 있게 된다.
     
     
     
     

    1.5 STL 맛보기

    이 절에서는 표준 C++ 라이브러리의 대부분을 차지하는 STL에 대한 감을 잡기 위해 간단한 프로그램을 예로 들어 설명한다. 일단, 평범한 C++ 프로그램에서 시작하여 표준 C++ 라이브러리가 제공하는 특징들을 하나씩 적용해 가면서, 프로그램을 버전업하기로 하자.

    1.5.1 프로그램 1: STL을 전혀 사용하지 않음

    다음은 정수값들을 입력으로 받아들여서, 이들을 정렬하고, 결과를 출력하는 아주 간단한 C++ 프로그램이다.
      #include <stdlib.h>
      #include <iostream>
      
      // qsort()의 인자로 쓰일 비교함수
      inline int cmp(const void *a, const void *b)
      {
          int aa = *(int *)a;
          int bb = *(int *)b;
          return (aa < bb) ? -1 : (aa > bb) ? 1 : 0;
      }
      
      int main()
      {
          const int size = 1e5;
          int array[size];        // 100,000개의 정수로 이루어진 배열
      
          // 입력
          int n = 0;
          while (cin >> array[n])
              n++;
          n--;
      
          // 정렬
          qsort(array, n, sizeof(int), cmp);
      
          // 출력
          for (int i = 0; i < n; i++)
              cout << array[i] << endl;
      }

    1.5.2 프로그램 2: 컨테이너, 반복자, 알고리듬

    STL은 여러 종류의 컨테이너들을 제공한다. 컨테이너란 객체들을 담아둘 수 있는 객체를 말하는데, 여기서는 vector를 사용해보도록 하자. vector는 배열과 비슷하지만, 필요에 따라 스스로 사이즈를 늘릴 수 있다는 점에서 배열과 차이가 난다. 따라서, 선언할 때 배열처럼 미리 사이즈를 정해주지 않아도 된다. vectorpush_back() 연산을 제공하는데, 이 연산은 vector의 맨 뒤에 원소를 집어 넣는 일을 한다. size() 멤버함수는 vector에 담긴 원소의 갯수를 반환한다.
    STL에서는 sort() 알고리듬을 이용하여 컨테이너를 정렬한다. 앞에서도 말했지만, STL은 객체지향적으로 설계된 라이브러리와는 달리, 알고리듬과 컨테이너를 완전히 분리하고 있다. 다시 말해서, vector 컨테이너만을 위한 정렬 알고리듬이 따로 있는 것이 아니라, 컨테이너 종류와는 무관하게(보통 'orthogonal'이라는 용어를 사용) 사용할 수 있는 알고리듬을 제공한다는 것이다.
    알고리듬을 컨테이너에 적용하기 위해서 필요한 것이 바로 반복자(iterator)이다. 반복자는 컨테이너내의 특정 위치를 가리키는 포인터와 비슷한 개념이라고 보면 되며, 이 반복자를 통해 컨테이너내의 원소들을 다양한 방법으로 다루게 되는 것이다. 다시말해, 반복자는 알고리듬과 컨테이너를 연결하는 매개체이다. begin()은 컨테이너의 맨 첫번째 원소의 위치를 가리키는 반복자를 리턴한다. 비슷하게, end()는 컨테이너의 맨 마지막 원소의 위치를 가리키는 반복자를 리턴한다. 이 두 반복자와 알고리듬을 이용하여 컨테이너의 원소들을 정렬한다. 방금 설명한 것들을 이용하여 프로그램 1을 다음과 같이 고칠 수 있다.
      #include <iostream>
      #include <vector>
      #include <algorithm>
      
      using namespace std;
      
      int main()
      {
          vector<int> v;
      
          // 입력
          int input;
          while (cin >> input)
              v.push_back(input);
      
          // 정렬
          sort(v.begin(), v.end());
      
          // 출력
          for (int i = 0; i < v.size(); i++)
              cout << v[i] << endl;
      }
    컨테이너(container), 알고리듬(algorithm), 반복자(iterator) 이 세가지가 STL에서 가장 중요한 것들이며, 특히 반복자를 능숙하게 다루어야 STL을 잘 활용할 수 있을 것이다.

    1.5.3 프로그램 3: 반복자 어댑터

      #include <iostream>
      #include <vector>
      #include <algorithm>
      
      using namespace std;
      
      int main()
      {
          vector<int> v;
          istream_iterator<int> start(cin), end;
          back_insert_iterator<vector<int> > dest(v);
      
          // 입력
          copy(start, end, dest);
      
          // 정렬
          sort(v.begin(), v.end());
      
          // 출력
          copy(v.begin(), v.end(), ostream_iterator<int>(cout, "n"));
      }
      출처:http://oopsla.snu.ac.kr/~sjjung/stl/var_0565.htm