[11장] STL(표준 템플릿 라이브러리): 효율적인 데이터 관리

STL, 즉 표준 템플릿 라이브러리는 C++ 프로그래밍 언어의 핵심 부분입니다. STL은 데이터 구조, 알고리즘, 반복자 등을 표준화한 라이브러리로서, 개발자들이 데이터를 더 효율적으로 관리하고 조작할 수 있도록 지원합니다. STL의 주요 목적은 코드의 재사용성과 추상화 수준을 높여, 개발자가 더 복잡한 문제에 집중할 수 있게 하는 것입니다.

클릭하시면 확대한 이미지를 확인하실 수 있습니다.

1. STL의 개요

윤성우의 열혈 C++ 프로그래밍, 오렌지미디어

STL, 즉 표준 템플릿 라이브러리는 C++ 프로그래밍 언어의 핵심 부분입니다. STL은 데이터 구조, 알고리즘, 반복자 등을 표준화한 라이브러리로서, 개발자들이 데이터를 더 효율적으로 관리하고 조작할 수 있도록 지원합니다. STL의 주요 목적은 코드의 재사용성과 추상화 수준을 높여, 개발자가 더 복잡한 문제에 집중할 수 있게 하는 것입니다.

STL의 주요 구성 요소

STL은 크게 세 가지 주요 구성 요소로 나뉩니다:

  1. 컨테이너(Container): 데이터를 저장하는 객체들로, 벡터(vector), 리스트(list), 맵(map) 등이 있습니다.
  2. 알고리즘(Algorithm): 데이터를 처리하는 표준화된 방법들로, 정렬(sort), 검색(search), 변환(transform) 등의 함수들을 포함합니다.
  3. 반복자(Iterator): 컨테이너 내의 데이터를 순회하는 객체로, 포인터와 유사한 방식으로 동작합니다.

STL의 실제 사용 예시

STL을 사용하는 간단한 예시를 통해 그 효율성을 살펴보겠습니다. 다음은 벡터 컨테이너를 사용하여 일련의 숫자를 저장하고, STL의 정렬 알고리즘을 사용하여 이를 정렬하는 코드입니다.

#include <iostream>
#include <vector>
#include <algorithm> // STL 알고리즘을 사용하기 위한 헤더

int main() {
    // 벡터 컨테이너 초기화
    std::vector<int> numbers = {7, 3, 5, 2, 9, 1};

    // STL sort 알고리즘을 사용하여 벡터 정렬
    std::sort(numbers.begin(), numbers.end());

    // 정렬된 벡터 출력
    for(int num : numbers) {
        std::cout << num << " ";
    }
    return 0;
}
C++20: 풍부한 예제로 익히는 핵심 기능, 인사이트

이 코드는 먼저 std::vector를 사용하여 숫자를 저장합니다. 이후 std::sort 함수를 사용하여 벡터 내의 숫자들을 오름차순으로 정렬합니다. 마지막으로, 벡터의 각 요소를 순회하며 출력합니다.

STL은 C++ 개발자들이 데이터를 효율적으로 관리하고 조작할 수 있게 해주는 강력한 도구입니다. 이를 통해 개발자는 보다 복잡하고 도전적인 문제를 해결하는 데 집중할 수 있으며, 코드의 재사용성과 유지 보수성을 크게 향상시킬 수 있습니다.

2. 알아두면 유용한 STL 스토리

시퀀스 컨테이너 (Sequence Containers)

시퀀스 컨테이너는 요소가 엄격한 순서대로 저장되는 컨테이너입니다. 주요 종류에는 vector, list, deque 등이 있습니다.

Vector

std::vector는 동적 배열을 제공합니다. 크기가 변경 가능하며, 요소에 대한 빠른 임의 접근이 가능합니다.

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5}; // 정수 벡터 초기화

    // 벡터에 요소 추가
    v.push_back(6);

    // 벡터의 모든 요소 출력
    for(int i : v) {
        std::cout << i << " ";
    }

    return 0;
}

List

std::list는 이중 연결 리스트를 제공합니다. 양방향 순회가 가능하며, 중간에 요소를 추가하거나 제거하는 것이 빠릅니다.

#include <list>
#include <iostream>

int main() {
    std::list<int> l = {1, 2, 3, 4, 5}; // 리스트 초기화

    // 리스트의 맨 앞에 요소 추가
    l.push_front(0);

    // 리스트의 모든 요소 출력
    for(int i : l) {
        std::cout << i << " ";
    }

    return 0;
}

Deque

std::deque는 양쪽 끝에서의 추가 및 제거가 가능한 시퀀스 컨테이너입니다.

#include <deque>
#include <iostream>

int main() {
    std::deque<int> d = {2, 3, 4}; // 데크 초기화

    // 데크의 앞과 뒤에 요소 추가
    d.push_front(1);
    d.push_back(5);

    // 데크의 모든 요소 출력
    for(int i : d) {
        std::cout << i << " ";
    }

    return 0;
}
초보자를 위한 C++ 200제:C++시작을위한최고의입문서! 설치부터문법배우고JSON응용까지레벨업!, 정보문화사

연관 컨테이너 (Associative Containers)

연관 컨테이너는 키를 기반으로 데이터를 저장하고 빠르게 검색할 수 있는 컨테이너입니다. 주로 setmap이 여기에 속합니다.

Set

std::set은 중복을 허용하지 않는 정렬된 집합을 제공합니다.

#include <set>
#include <iostream>

int main() {
    std::set<int> s = {3, 1, 5, 4, 2}; // 세트 초기화

    // 세트에 요소 추가 (중복은 자동으로 제거됨)
    s.insert(1); // 이미 존재하는 요소
    s.insert(6);

    // 세트의 모든 요소 출력
    for(int i : s) {
        std::cout << i << " ";
    }

    return 0;
}

Map

std::map은 키-값 쌍을 저장하는 정렬된 맵을 제공합니다. 각 키는 유일합니다.

#include <map>
#include <iostream>

int main() {
    std::map<std::string, int> m; // 맵 초기화

    // 맵에 키-값 쌍 추가
    m["apple"] = 5;
    m["banana"] = 3;
    m["orange"] = 4;

    // 맵의 모든 요소 출력
    for(auto& pair : m) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    return 0;
}
이것이 C++이다:강의 현장을 그대로 옮긴 C++ 입문서, 한빛미디어

어댑터 컨테이너 (Container Adapters)

어댑터 컨테이너는 특정한 인터페이스를 제공하는 컨테이너입니다. stack, queue, priority_queue 등이 여기에 속합니다.

Stack

std::stack은 LIFO(후입선출) 방식의 스택을 구현합니다.

#include <stack>
#include <iostream>

int main() {
    std::stack<int> s; // 스택 초기화

    // 스택에 요소 추가
    s.push(1);
    s.push(2);
    s.push(3);

    // 스택의 상단 요소 출력 후 제거
    while (!s.empty()) {
        std::cout << s.top() << " ";
        s.pop();
    }

    return 0;
}

Queue

std::queue는 FIFO(선입선출) 방식의 큐를 구현합니다.

#include <queue>
#include <iostream>

int main() {
    std::queue<int> q; // 큐 초기화

    // 큐에 요소 추가
    q.push(1);
    q.push(2);
    q.push(3);

    // 큐의 맨 앞 요소 출력 후 제거
    while (!q.empty()) {
        std::cout << q.front() << " ";
        q.pop();
    }

    return 0;
}

Priority Queue

std::priority_queue는 우선순위에 따라 요소를 정렬하는 큐입니다.

#include <queue>
#include <iostream>

int main() {
    std::priority_queue<int> pq; // 우선순위 큐 초기화

    // 우선순위 큐에 요소 추가
    pq.push(3);
    pq.push(1);
    pq.push(4);
    pq.push(2);

    // 우선순위가 가장 높은 요소부터 출력 후 제거
    while (!pq.empty()) {
        std::cout << pq.top() << " ";
        pq.pop();
    }

    return 0;
}
이것이 C++이다:강의 현장을 그대로 옮긴 C++ 입문서, 한빛미디어

이러한 각 컨테이너들은 STL을 사용하여 데이터를 효율적으로 관리하고 조작하는 데 필수적인 도구입니다.

알고리즘

STL(Standard Template Library)의 알고리즘들은 데이터를 처리하고 조작하는 데 사용되는 다양한 함수들로 구성되어 있으며, 컨테이너의 유형에 관계없이 범용적으로 사용될 수 있습니다. 아래에서는 주요 알고리즘들에 대한 자세한 설명과 주석이 달린 코드 예시를 제공합니다.

정렬 (Sorting)

정렬 알고리즘은 데이터를 특정 기준에 따라 순서대로 나열하는 데 사용됩니다.

sort

std::sort는 시퀀스를 오름차순 또는 내림차순으로 정렬합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {4, 1, 3, 5, 2};
    
    // 벡터를 오름차순으로 정렬
    std::sort(v.begin(), v.end());

    for(int i : v) {
        std::cout << i << " "; // 1 2 3 4 5
    }

    return 0;
}
partial_sort

std::partial_sort는 컨테이너의 일부를 정렬하는 데 사용됩니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {4, 1, 3, 5, 2};
    
    // 벡터의 처음 3개 요소를 오름차순으로 정렬
    std::partial_sort(v.begin(), v.begin() + 3, v.end());

    for(int i : v) {
        std::cout << i << " "; // 1 2 3 5 4
    }

    return 0;
}
nth_element

std::nth_element는 n번째 요소를 찾고, 이를 기준으로 시퀀스를 분할합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {4, 1, 3, 5, 2};
    
    // 벡터에서 3번째로 작은 요소를 찾아 위치시킴
    std::nth_element(v.begin(), v.begin() + 2, v.end());

    for(int i : v) {
        std::cout << i << " "; // 1 2 3 4 5
    }

    return 0;
}
이것이 C++이다:강의 현장을 그대로 옮긴 C++ 입문서, 한빛미디어

검색 (Searching)

검색 알고리즘은 데이터 내에서 특정 요소를 찾는 데 사용됩니다.

find

std::find는 주어진 값과 일치하는 첫 번째 요소의 반복자를 반환합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    auto it = std::find(v.begin(), v.end(), 3);

    if(it != v.end()) {
        std::cout << "Found: " << *it << std::endl; // Found: 3
    } else {
        std::cout << "Not found" << std::endl;
    }

    return 0;
}
binary_search

std::binary_search는 이진 검색 알고리즘을 사용하여 요소의 존재 여부를 확인합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};

    if(std::binary_search(v.begin(), v.end(), 4)) {
        std::cout << "Found" << std::endl; // Found
    } else {
        std::cout << "Not found" << std::endl;
    }

    return 0;
}
lower_bound / upper_bound

std::lower_boundstd::upper_bound는 범위 내에서 주어진 값과 일치하거나 큰(작은) 첫 번째 요소를 찾습니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 3, 3, 4, 5};
    
    auto lower = std::lower_bound(v.begin(), v.end(), 3);
    auto upper = std::upper_bound(v.begin(), v.end(), 3);

    std::cout << "Lower bound: " << (lower - v.begin()) << std::endl; // 2
    std::cout << "Upper bound: " << (upper - v.begin()) << std::endl; // 5

    return 0;
}
이것이 C#이다 단계별 학습으로 탄탄한 기본기를 다져줄 C# 입문서 3판, 한빛미디어

수정 (Modifying)

수정 알고리즘은 데이터 내의 요소를 변경하는 데 사용됩니다.

copy

std::copy는 한 컨테이너에서 다른 컨테이너로 요소를 복사합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> src = {1, 2, 3, 4, 5};
    std::vector<int> dest(5);

    std::copy(src.begin(), src.end(), dest.begin());

    for(int i : dest) {
        std::cout << i << " "; // 1 2 3 4 5
    }

    return 0;
}
replace

std::replace는 지정된 값을 다른 값으로 교체합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};

    std::replace(v.begin(), v.end(), 3, 8);

    for(int i : v) {
        std::cout << i << " "; // 1 2 8 4 5
    }

    return 0;
}
remove

std::remove는 지정된 값을 제거합니다. (실제로 컨테이너에서 제거하지는 않고, 끝으로 이동시킵니다.)

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 3, 5};

    auto new_end = std::remove(v.begin(), v.end(), 3);
    v.erase(new_end, v.end()); // 실제로 제거

    for(int i : v) {
        std::cout << i << " "; // 1 2 4 5
    }

    return 0;
}
transform

std::transform은 주어진 함수를 컨테이너의 각 요소에 적용합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    std::vector<int> result(v.size());

    std::transform(v.begin(), v.end(), result.begin(), [](int x) { return x * x; });

    for(int i : result) {
        std::cout << i << " "; // 1 4 9 16 25
    }

    return 0;
}
이것이 C#이다 단계별 학습으로 탄탄한 기본기를 다져줄 C# 입문서 3판, 한빛미디어

순회(Traversal)

순회(Traversal) 알고리즘은 STL에서 컨테이너의 요소들을 순차적으로 접근하고 처리하는 데 사용됩니다. 여기에는 for_each, accumulate, count 등이 포함되며, 각각의 알고리즘은 특정한 작업을 수행합니다.

for_each

std::for_each 알고리즘은 주어진 범위의 모든 요소에 대해 함수를 적용합니다. 이 함수는 반복자를 통해 각 요소에 접근하여 작업을 수행합니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};

    // 모든 요소에 대해 람다 함수 적용
    std::for_each(v.begin(), v.end(), [](int &n) {
        n *= 2; // 각 요소를 두 배로 만듦
    });

    // 결과 출력
    for (int n : v) {
        std::cout << n << " ";
    }

    return 0;
}
accumulate

std::accumulate 알고리즘은 주어진 범위의 모든 요소를 초기 값에 누적하여 합산합니다. 기본적으로 덧셈을 수행하지만, 다른 연산을 적용할 수도 있습니다.

#include <numeric>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};

    // 벡터의 모든 요소 합산
    int sum = std::accumulate(v.begin(), v.end(), 0);

    std::cout << "Sum: " << sum << std::endl;

    return 0;
}
count

std::count 알고리즘은 주어진 범위 내에서 특정 값과 일치하는 요소의 수를 세는 데 사용됩니다.

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5, 4, 4};

    // 값 4의 출현 횟수 카운트
    int count_4 = std::count(v.begin(), v.end(), 4);

    std::cout << "Number of 4s: " << count_4 << std::endl;

    return 0;
}

이러한 순회 알고리즘들은 다양한 컨테이너 유형에 적용될 수 있으며, 데이터를 조작하거나 정보를 추출하는 데 매우 유용합니다.

명품 C++ Programming:눈과 직관만으로도 누구나 쉽게 이해할 수 있는 명품 C++ 강좌, 생능출판

반복자(Iterators)

반복자(Iterators)는 STL에서 컨테이너의 요소에 접근하고 순회하는 데 사용되는 객체입니다. 반복자는 포인터와 유사하게 작동하지만, 다양한 종류의 컨테이너와 호환됩니다. 여기서 각 반복자 유형에 대한 설명과 예제 코드를 제공하겠습니다.

입력 반복자 (Input Iterators)

입력 반복자(Input Iterators)는 STL에서 읽기 전용 접근을 제공하는 가장 기본적인 반복자 유형입니다. 이 반복자는 컨테이너의 요소를 순차적으로 읽는 데 사용되며, 주로 데이터를 읽거나 단순한 순회를 수행할 때 활용됩니다.

입력 반복자의 주요 특징은 다음과 같습니다:

  • 단방향 이동: 입력 반복자는 오로지 전방으로만 이동할 수 있습니다.
  • 단일 통과: 각 요소는 한 번만 통과하며, 다시 돌아갈 수 없습니다.
  • 읽기 전용: 요소를 읽을 수는 있지만 수정할 수는 없습니다.

아래 코드 예제에서는 std::istream_iterator, 한 종류의 입력 반복자를 사용하여 표준 입력에서 정수들을 읽어들이고, 이를 벡터에 저장하는 과정을 보여줍니다.

#include <iostream>
#include <vector>
#include <iterator>

int main() {
    std::cout << "Enter numbers (EOF to stop): ";

    // 표준 입력으로부터 정수를 읽는 입력 반복자 생성
    std::istream_iterator<int> start(std::cin), end;

    // 입력 반복자를 사용하여 벡터 초기화
    std::vector<int> numbers(start, end);

    std::cout << "Read numbers: ";
    for (int n : numbers) {
        std::cout << n << " ";
    }

    return 0;
}

이 코드에서 std::istream_iterator<int>는 표준 입력으로부터 정수를 읽어들이는 입력 반복자입니다. startend 반복자를 사용하여 벡터 numbers를 초기화하고, 이후 벡터의 모든 요소를 출력합니다. 사용자가 EOF(End of File) 신호를 보내면 입력이 종료됩니다.

[영진닷컴]그림으로 배우는 C++ Programming - 2nd Edition, 영진닷컴

이 예제는 입력 반복자가 어떻게 데이터를 읽는 데 사용될 수 있는지 보여줍니다. 특히, 입력 스트림을 통해 데이터를 읽어오는 과정에서 이 반복자가 얼마나 유용한지 확인할 수 있습니다.

출력 반복자 (Output Iterators)

출력 반복자(Output Iterators)는 STL에서 쓰기 전용 접근을 제공하는 반복자입니다. 이들은 데이터를 쓰거나 수정하는 데 사용되며, 주로 컨테이너의 요소에 새로운 값을 할당할 때 활용됩니다.

출력 반복자의 주요 특징은 다음과 같습니다:

  • 단방향 이동: 출력 반복자는 오로지 전방으로만 이동할 수 있습니다.
  • 단일 통과: 각 위치는 한 번만 통과할 수 있으며, 반복적인 접근이 불가능합니다.
  • 쓰기 전용: 요소에 값을 할당할 수 있지만, 읽을 수는 없습니다.

다음 코드 예제에서는 std::ostream_iterator, 출력 반복자의 한 종류를 사용하여 벡터의 내용을 표준 출력에 쓰는 과정을 보여줍니다.

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 표준 출력에 쓰기 위한 출력 반복자 생성
    std::ostream_iterator<int> out_it(std::cout, " ");

    // 벡터의 모든 요소를 표준 출력에 쓰기
    std::copy(numbers.begin(), numbers.end(), out_it);

    return 0;
}
[영진닷컴]그림으로 배우는 C++ Programming - 2nd Edition, 영진닷컴

이 코드에서 std::ostream_iterator<int>는 표준 출력으로 정수를 쓰는 출력 반복자입니다. 이 반복자는 std::copy 알고리즘과 함께 사용되어 벡터 numbers의 모든 요소를 표준 출력에 순차적으로 씁니다. 각 요소 사이에는 공백 문자(” “)가 출력됩니다.

이 예제는 출력 반복자가 데이터를 쓰는 데 어떻게 사용될 수 있는지 보여줍니다. 특히, 표준 출력 스트림과 결합하여 데이터를 쉽고 효율적으로 출력할 수 있음을 확인할 수 있습니다.

전방 반복자 (Forward Iterators)

전방 반복자는 단방향 이동이 가능합니다. 입력 및 출력 작업 모두 수행할 수 있습니다.

#include <forward_list>
#include <iostream>

int main() {
    std::forward_list<int> fl = {1, 2, 3, 4, 5};

    // 전방 반복자를 사용한 읽기 및 쓰기
    for(auto it = fl.begin(); it != fl.end(); ++it) {
        *it = *it * 2; // 값을 두 배로 증가
        std::cout << *it << " ";
    }

    return 0;
}

이 코드는 forward_list의 전방 반복자를 사용하여 각 요소를 두 배로 증가시키고, 결과를 출력합니다.

양방향 반복자 (Bidirectional Iterators)

양방향 반복자는 양방향 이동이 가능합니다. 리스트(list)나 세트(set) 같은 컨테이너에서 사용됩니다.

#include <list>
#include <iostream>

int main() {
    std::list<int> l = {1, 2, 3, 4, 5};

    // 양방향 반복자를 사용하여 역순으로 순회
    for(auto it = l.rbegin(); it != l.rend(); ++it) {
        std::cout << *it << " ";
    }

    return 0;
}

이 코드는 list의 양방향 반복자를 사용하여 리스트를 역순으로 순회합니다.

임의 접근 반복자 (Random-Access Iterators)

임의 접근 반복자는 벡터(vector)나 배열(array) 같은 컨테이너에서 사용되며, 임의 접근이 가능합니다.

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};

    // 임의 접근 반복자를 사용한 무작위 접근
    for (int i = v.size() - 1; i >= 0; --i) {
        std::cout << v[i] << " "; // 역순으로 요소에 접근
    }

    return 0;
}
A Tour of C++, 에이콘출판

이 코드는 벡터의 임의 접근 반복자를 사용하여 역순으로 요소에 접근합니다. 위 예제에서 i를 순차적으로 부르고 있지만, 렌덤함 값을 너으면 렌덤함 순으로 v속 값을 가지고 올 수 있습니다.

이러한 반복자들은 STL 컨테이너의 요소에 접근하고 순회하는 데 필수적인 도구입니다. 다양한 컨테이너 유형에 따라 적합한 반복자를 선택하여 사용할 수 있습니다.

3. 결론

STL(표준 템플릿 라이브러리)은 C++ 프로그래밍에서 데이터를 효율적으로 관리하고 조작하는 데 필수적인 도구입니다. 다양한 컨테이너는 데이터 저장 및 구조화에, 알고리즘은 데이터 처리 및 조작에, 반복자는 데이터 접근 및 순회에 각각 중요한 역할을 합니다. 이들을 적절히 활용함으로써, 코드의 재사용성과 유지 보수성을 향상시킬 수 있으며, 복잡한 문제를 보다 쉽게 해결할 수 있습니다. 결국, STL은 C++ 개발자들이 더 효율적이고 강력한 소프트웨어를 개발하는 데 중요한 기반이 됩니다.