[전문가를 위한 C++] 17장. 컨테이너와 반복자 살펴보기

Chapter 17. 컨테이너와 반복자 살펴보기

컨테이너 개요

  • 다양한 데이터를 묶음 단위로 저장하는 제네릭 데이터 구조다.
  • 표준 라이브러리의 컨테이너는 모두 클래스 템플릿으로 구현되어 있어 기본 조건만 만족한다면(원소에 대한 요구사항) 거의 모든 타입에 대해 인스턴스화 하여 사용할 수 있다.
  • array와 bitset을 제외한 표준 라이브러리 컨테이너는 모두 크기를 조절할 수 있고 추가 삭제에 따라 크기가 자동으로 결정된다.
    • vector (동적 배열)
    • deque
    • list
    • forward_list
    • array
    • map
    • multimap
    • set
    • multiset
    • unordered_map
    • unordered_multimap
    • unordered_list
    • unordered_multilist
    • queue
    • priority_queue
    • stack
    • 순차 컨테이너
    • 연관 컨테이너
    • 비정렬 연관 컨테이너 (해시 테이블)
    • 컨테이너 어댑터
  • 표준 라이브러리에 있는 것들은 모두 std 네임스페이스에 속한다.
    • using namespace std; (이 문장을 절대로! 헤더 파일에 넣으면 안 된다.)

원소에 대한 요구사항

  • 표준 라이브러리 컨테이너는 원소를 값으로 처리한다. (value semantics)
    • 원소의 복제본을 작성하고, 대입 연산자로 대입하고, 소멸자로 원소를 삭제한다. 그래서 표준 라이브러리를 사용하는 클래스를 작성할 때는 반드시 복제할 수 있도록 만들어야 한다.
  • 표준 라이브러리 컨테이너에서 원소를 요청하면 저장된 복제본에 대한 레퍼런스를 리턴한다.
  • 원소를 레퍼런스로 처리하고 싶다면,
    • 원소를 그대로 넣지 말고 원소에 대한 포인터를 저장한다.
    • 혹은 컨테이너에 std::reference_wrapper을 저장한다. 이것은 std::ref(), std::cref()로 생성되며, <functional> 헤더 파일에 정의 되어 있다.
  • 표준 라이브러리 컨테이너에 대한 템플릿 타입 매개변수 중에는 할당자(allocator)라는 것이 있다.
    • 원소에 대한 메모리를 할당하거나 해제할 수 있다.
  • map과 같은 일부 컨테이너는 템플릿 타입 매개변수로 비교자도 받을 수 있다.
    • 원소를 정렬하는 데 사용.
  • 요구사항은 다음과 같다.
    • 복제 생성자
    • 이동 생성자
    • 대입 연산자
    • 이동 대입 연산자
    • 소멸자
    • 디폴트 생성자
    • operator==
    • operator<

익셉션과 에러 검사

  • 표준 라이브러리 컨테이너는 에러 검사 기능을 제공한다.

반복자

  • 입력
  • 출력
  • 정방향
  • 양방향
  • 랜덤 액세스 (임의 접근)

순차 컨테이너

  • vector
    • 원소를 연속된 메모리 공간에 한 자리 씩 차지하게 저장한다.
    • vector 개요
      • <vector> 헤더 파일에 정의된 클래스 템플릿으로, 저장할 원소의 타입과 할당자 타입을 매개변수로 담는다.

        template<class T, class Allocator = allocator<T> >> class vector;
        
      • Allocator 매개변수는 클라이언트가 메모리 할당 방식을 커스터마이즈하는 데 사용된다. 디폴트로 정해져 있다.

    • 동적 크기 vector
      • push_back() 메서드로 vector에 추가한다. 새로 추가한 원소에 대한 메모리는 vector가 알아서 할당한다.
    • vector 세부 기능
      • 생성자와 소멸자

          // 원소 수와 각각의 초깃값을 지정할 수 있다.
          vector<int> intvector(10, 100);  // 초깃값이 100인 int 원소 10개를 담은 vector 생성
        
          // vector를 생성할 때 초기 원소를 이니셜라이저리스트(initializer_list)로 지정해도 된다.
          vector<int> intvector({1, 2, 3, 4, 5});
        
          // 유니폼 초기화 사용 가능 
          vector<int> intvector1 = {1, 2, 3, 4, 5};
          vector<int> intvector2{1, 2, 3, 4, 5};
        
          // Heap에 할당 가능 
          auto Elementvector = make_unique<vector<Element>>;
        
    • vector 복제와 대입
      • vector는 객체의 복사본을 저장한다.
      • vector의 소멸자는 원소 객체의 소멸자를 호출하도록 되어 있다.
      • vector 클래스의 복제 생성자와 대입 연산자는 vector 원소를 복제할 때 깊은 복제를 수행한다.
        • 성능을 높이려면 메서드에 vector 전달 시 레퍼런스나 const 레퍼런스로 전달하는 것이 좋다.
      • assign() 메서드로 vector를 재사용할 수 있다. (이니셜라이저리스트 사용 가능)

        vector<int> vectorOne(10);
        vector<int> vectorTwo(5, 100);
        vectorOne.swap(vectorTwo);
        
      • swap() 메서드로 두 vector를 상수 시간에 맞 바꾼다.

        vector<int> vectorOne(10);
        vector<int> vectorTwo(5, 100);
        vectorOne.swap(vectorTwo);
        
    • vector 비교
      • 표준 라이브러리에서 제공하는 vector는 비교 연산자를 오버로딩한 버전을 제공한다.
      • 커스텀 클래스에서 객체를 저장한 vector를 비교하려면 반드시 커스텀 클래스에 비교 연산자가 오버로딩 구현 되어 있어야 한다.
      • vector 반복자
        • begin()은 컨테이너의 첫번째 원소를 참조하는 반복자를 리턴한다.

          for (vector<double>::iterator it = begin(douvlevector); it != end(doublevector); ++it) {...}
          
        • end()는 vector의 마지막 원소의 다음 원소를 가리키는 반복자를 리턴한다.

    • vector에 레퍼런스 저장하기
      • vector와 같은 컨테이너에 레퍼런스를 저장할 수 있다.

        string str1 = "Hello";
        string str2 = "World";
        
        vector<reference_wrapper<string>> vec{ref(str1)};
        vec.push_back(ref(str2));
              
        vec[1].get() += "!";  // 앞에서 만든 vector의 두 번째 원소(레퍼런스)가 참조하는 스트링 값을 변경한다.
        
    • 원소 추가와 삭제
      • insert() 메서드를 이용하면 원하는 지점에 원소를 추가할 수 있다.
        • 원소 하나만 추가
        • 원소 하나에 대한 n개의 복제본 추가
        • 반복자 범위로 지정된 원소들을 추가
        • 지정한 원소를 이동 의미론을 적용해서 vector에 이동시켜 추가
        • 원소 리스트를 initializer_list에 지정해서 vector에 추가
      • push_back(), insert()는 rvalue, lvalue를 매개변수로 받는 버전도 있다.
      • erase() 메서드를 이용하면 vector에 담긴 원소 중 원하는 것을 제거할 수 있다.
        • 반복자 하나를 인수로 받아 원소 하나만 삭제
        • 삭제할 원소 범위를 지정하여 두 개의 반복자를 인수로 받는다.
      • 원소를 모두 삭제하려면 clear()를 사용한다.
      • 이동 의미론
        • 표준 라이브러리에서 제공하는 컨테이너는 모두 이동 의미론을 구현하도록 이동 생성자와 이동 대입 연산자가 제공된다.
        • 따라서 표준 라이브러리 컨테이너를 함수에서 값으로 리턴해도 성능 오버헤드를 최소화 할 수 있다.
        • push 연산도 특정한 상황에서 이동 의미론을 적용해 성능을 높일 수 있다.

           vector<string> vec;
           string myElement(5, 'a');
          
           vec.push_back(myElement);  // myElement는 임시객체가 아니기 때문에 복제본을 만들어 vector에 추가한다.
           vec.push_back(move(myElement));  // push_back(const T& val)의 이동 의미 버전인 push_back(T&& val)도 지원하므로 이렇게 호출하면 복제 연산이 발생하지 않는다.
          
           // 다음과 같이 push_back을 호출하면 이동 의미론 버전이 호출된다. 여기서 호출한 string 생성자는 임시 객체를 생성하기 때문이다. 
           // push_back 메서드는 이렇게 생성된 임시 string 객체를 vector로 복제하지 않고 이동시킨다.
           vec.push_back(string(5, 'a'));
          
    • 임플레이스 연산
      • 특정한 지점에 설치한다. 복제나 이동 작업을 수행하지 않고, 컨테이너에 공간을 마련해서 객체를 그 자리에서 바로 생성한다.

        vec.emplace_back(5, 'a');
        
    • 추가된 원소에 대한 레퍼런스를 리턴한다. (C++17)
    • 알고리즘 복잡도와 반복자 무효화
    • vector의 메모리 할당 방식
      • vector는 기본적으로 연속된 메모리 공간에 원소를 저장하며 원소 추가 시 메모리가 자동으로 할당된다.
      • 원소를 추가하다가 공간이 모자라면 더 큰 공간을 새로 할당 받아 기존 원소들을 모두 새 공간으로 이동하고 복제해야 한다.
      • 따라서 재 할당 발생 가능성을 최소화 하기 위해 실제로 필요한 양보다 더 많이 할당 받는다.
      • 재 할당이 발생하면 원소 추가 시 선형 시간의 복잡도를 가지게 된다. 그리고 기본 반복자들이 무효가 된다.
      • 따라서 vector는 vector의 재할당 상태를 조회하거나 직접 제어하는 인터페이스도 제공한다.
  • vector<bool> 특수화
    • 음… bitset쓰라고 권장함.
  • deque
    • <deque> 헤더 파일에 정의 되어 있다.
    • vector에 비해 안 쓰인다고 자세한 설명 안 한다고 함.
  • list
    • <list> 헤더 파일에 정의 되어 있는 이중 연결 리스트를 구현한 표준 라이브러리 클래스 템플릿.
    • 리스트의 모든 지점에서 원소를 추가하거나 삭제하는 속도는 상수 시간이지만, 각각의 원소를 조회하는 작업은 다소 느린 선형 시간에 처리한다.
    • operator[]처럼 랜덤 액세스 연산을 지원하지 않고, 반복자로만 개별 원소에 접근할 수 있다.
    • 생성자, 소멸자, 복제 연산자, 대입 연산자, 비교연산자 등 대부분은 vector와 같지만 다른 연산은 아래와 같다.
    • 원소 접근 연산
      • front(), back() : 리스트에 담긴 첫 번째와 마지막 원소에 대한 레퍼런스를 리턴한다. 나머지는 반복자로만 접근할 수 있다.
      • begin(), end()
      • cbegin(), cend(), rbegin(), rend(), crbegin(), crend()
    • 반복자
      • list는 랜덤 액세스를 지원하지 않기 때문에, list반복자끼리 더하거나 뺄 수 없고, 반복자를 가리키는 포인터에 대해 산술 연산을 할 수 없다.
      • list반복자에 대한 포인터 p가 있을 때, ++p, –p와 같은 연산으로 list 원소를 탐색할 수 있지만 p+n, p-n과 같이 덧셈이나 뺄셈 연산자를 적용할 수는 없다.
    • 원소 추가와 삭제 연산
      • push_back(), pop_back(), emplace(), emplace_back(), 다섯 가지 버전의 insert(), 두 가지 버전의 erase(), clear() 등.
    • list 크기와 관련된 연산
      • list는 vector와 다르게 내부 메모리 모델에 관련된 메서드가 없다.
      • size(), empty(), resize()는 제공하지만 reserve(), capacity()와 같은 메서드를 제공하지 않는다는 뜻이다.
    • list의 특수 연산
      • splace() : 이어붙이기

        auto iterLastB = --(cend(bWords));
        auto it = cbegin(dictionary);
        for (; it != cend(dictionary); ++i) {
            if (*it > *iterLastB) break;
        }
        dictionary.splice(it, bWords);  // bWords에 있던 원소가 삭제된다.
        for (const auto& word: dictionary) {
            cout << word << endl;
        }
        // splice()에는 두 가지 버전이 더 있다. 다른 리스트에 있는 원소 하나를 추가하는 버전, 다른 리스트 일부 원소들을 추가하는 버전.
        
      • 좀 더 효율적인 버전의 알고리즘

        • remove(), remove_if() : list에서 특정 원소를 제거한다.
        • unique() : list에서 같은 원소가 연달아 나오는 부분을 제거한다.
        • merge() : 두 리스트를 합친다. 둘 다 정렬된 상태여야 한다. 원본을 변경한다.
        • sort() : 리스트를 정렬한다.
        • reverse() : 리스트의 순서를 반대로 바꾼다.
  • forward_list
    • <forward_list>
  • array
    • 크기가 고정된 점을 제외하면 vector와 같다. 고정 시킨 이유는 원소를 스택에 할당하기 위해서다.
    • fill() 메서드로 array를 특정 원소로 채울 수 있다.
    • swap() 메서드 성능이 선형 시간이다. (벡터는 상수)
    • std::get() 함수 템플릿으로 std::array의 N인덱스에 있는 원소를 가져올 수 있다. 인덱스는 반드시 상수여야 한다.

      array<int, 3> myArray(11, 12, 13);
      cout << std::get<1>(myArray) << endl;
      cout << std::get<10>(myArray) << endl;  // compile error.
      

컨테이너 어댑터

  • 일종의 wrapper다. 내부적으로 순차 컨테이너를 사용한다.
  • queue
    • <queue>
    • queue 연산
    • push(), emplace() 메서드로 큐의 끝에 원소를 추가할 수 있다.
    • pop()은 큐의 앞에서 원소를 제거한다.
    • front(), back()을 이용하면 원소를 제거하지 않고도 맨 앞과 맨 뒤의 원소에 대한 레퍼런스를 구할 수 있다.
  • priority_queue
    • <queue>
    • 맨 앞(헤더)에 있는 원소의 우선순위가 가장 높다. 디폴트 설정에 따르면 이 순서는 operator< 연산에 따라 낮은 쪽이 낮은 우선순위를 갖도록 정한다.
    • priority_queue 연산
      • push(), emplace() 원소 추가,
      • pop() 원소 제거,
      • top() 맨 앞의 원소에 대한 const 레퍼런스 리턴
      • size(), empty(), swap()을 지원한다. 비교 연산자는 지원하지 않는다.
  • stack

정렬 연관 컨테이너

  • pair 유틸리티 클래스
  • map
  • multimap
  • set
  • multiset

비정렬 연관 컨테이너 (해시 테이블)

  • 해시 함수
  • unordered_map
  • unordered_multimap
  • unordered_set과 unordered_multimap

기타 컨테이너

  • 표준 C 스타일 배열
  • string
  • 스트림
  • bitset