STL 기초

주상돈·2025년 1월 2일

TIL

목록 보기
8/53

오늘은 STL에 대해 배울려고한다 STL은Standard Template Libarary로 C++에서 자체적으로 제공하는 표준 라이브러리이다.
STL에는 개발 시 유용하게 사용되는 컨테이너, 알고리즘이 구현되어 있기 때문에, 개발자는 헤더파일을 추가하고 사용하기만 하면 된다.

컨테이너

컨테이너는 데이터를 담는 자료구조를 말한다.
데이터를 담는 방식이나 제공하는 메서드에 따라 여러가지 컨테이너를 제공한다.
1. 모든 컨테이너는 템플릿으로 구현되어 있다. 즉 타입에 상관없이 사용 가능 하다.
2. 모든 컨테이너는 메모리 관리를 내부적으로 한다. 따라서 사용시 메모리 관리를 고려하지 않아도 된다.
3. 모든 컨테이너는 반복자를 가지고 있다. 따라서 컨테이너 내부 세부적인 구현사항을 알지 못해도 모든 컨테이너를 동일한 문법으로 접근할 수 있다.

벡터

벡터는 배열과 매우 유사한 컨테이너이다.
대표적인 특징으로는 아래와 같다.

1️⃣ 타입에 종속되지 않는 템플릿 클래스로 구현 됨
2️⃣ 삽입되는 원소 개수에 따라 내부 배열의 크기가 자동 관리 됨
3️⃣ 임의접근이 가능함(인덱스를 톻해 특정 위치에 접근)
4️⃣ 삽입/삭제는 맨 뒤에 하는게 좋음(배열의 특성)

벡터의 선언

1️⃣ 기본생성&특정값으로 초기화

기본 생성 하거나 특정값으로 초기화 하는 코드 이다. vec는 빅 벡터이므로 크기가 0이고, vec2의 크기는 5이다.

#include <vector>
using namespace std;

// 1. 기본 생성 및 초기화 없이 정의
vector<int> vec1;

// 2. 특정 크기와 초기값으로 벡터 정의
vector<int> vec2(5, 10); // 크기 5, 모든 원소가 10으로 초기화

//메인 함수 생략

2️⃣ 리스트 초기화로 백터를 정의하는 코드이다.
특정값으로 초기화 하고 싶을때 많이 사용한다.
값이 많을때는 사용하기 어렵ㄱ ㅗ초기화하는 값의 개수가 적을 때 보통 사용한다.

#include <vector>
using namespace std;

// 3. 리스트 초기화로 벡터 정의
vector<int> vec3 = {1, 2, 3, 4, 5};

//메인 함수 생략

3️⃣ 다른 벡터의 복사 하거나 대입하는 방법도 있다.
기존에 생성된 벡터의 복사본을 만들 때 많이 사용 한다.

#include <vector>
using namespace std;

// 다른 벡터를 기반으로 복사 초기화
vector<int> vec3 = {1, 2, 3, 4, 5};
vector<int> vec4(vec3); // vec3의 복사본 생성
//vector<int> vec4 = vec3 하면 대입이 됨
//메인 함수 생략

4️⃣ 2차원 배열처럼 벡터를 사용하려면, 벡터의 타입을 벡터로 하면 된다.
아래와 같이 하면 2차원 벡터처럼 사용할 수 있다.

#include <vector>
using namespace std;

// 2차원 벡터 초기화
vector<vector<int>> vec2D(3, vector<int>(4, 7)); // 3x4 행렬, 모든 원소가 7로 초기화

//메인 함수 생략

벡터의 동작

1️⃣ push_back
벡터의 맨 끝에 원소를 추가하는 메서드 이다.
원소의 개수가 늘어남에 따라 크기는 자동으로 증가하기 때문에 신경쓸 필요 없다.
아래는 예시이다.

#include <iostream>
#include <vector>
using namespace std;

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

    cout << "Vector after push_back: ";
    for (int num : vec) {
        cout << num << " ";
    }
    return 0;
}
// 출력: Vector after push_back: 10 20 30

2️⃣ pop_back
벡터의 맨 끝에 원소를 제거하는 메서드 이다.
벡터의 맨 끝 원소가 제거되므로 크기가 줄어 든다.
아래는 예시이다.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> vec = {10, 20, 30};
    vec.pop_back();  // 마지막 요소(30) 제거

    cout << "Vector after pop_back: ";
    for (int num : vec) {
        cout << num << " ";
    }
    return 0;
}
// 출력: Vector after pop_back: 10 20

3️⃣ size
현재 벡터의 크기를 확인할 때 사용한다.
보통 벡터의 전체 원소에 대해 무언가 해야할 때 유용하게 사용된다.
아래는 예시코드이다.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> vec = {10, 20, 30};
    cout << "Size of vector: " << vec.size() << endl;

    vec.push_back(40);
    cout << "Size after push_back: " << vec.size() << endl;

    vec.pop_back();
    cout << "Size after pop_back: " << vec.size() << endl;

    return 0;
}
// 출력:
// Size of vector: 3
// Size after push_back: 4
// Size after pop_back: 3

4️⃣ erase
특정위치의 원소를 제거하는 함수이다.
벡터의 특성상 시간이 많이 걸리는 함수 이므로 되도록이면 사용하지 않는게 좋다.
아래는 예시코드이다.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> vec = {10, 20, 30, 40, 50};

    // 두 번째 요소 제거 (index 1)
    vec.erase(vec.begin() + 1);

    cout << "Vector after erasing second element: ";
    for (int num : vec) {
        cout << num << " ";
    }
    cout << endl;

    // 2~4 번째 요소 제거 (index 1~3)
    vec.erase(vec.begin() + 1, vec.begin() + 4);

    cout << "Vector after erasing range: ";
    for (int num : vec) {
        cout << num << " ";
    }
    return 0;
}
// 출력:
// Vector after erasing second element: 10 30 40 50
// Vector after erasing range: 10 50

배열은 인덱스를 활용해서 특정 위치의 값을 찾아줬다면, 맵은 인덱스 대신 키(Key)를 사용하고 값과 쌍을 이루고 있는 것을 볼 수 있다.
맵의 주요한 특성은 아래와 같다.
1️⃣ 키-값 쌍이 하나의 타입이 된다.
2️⃣ 키 값을 기준으로 데이터가 자동 정렬된다.
3️⃣ 중복된 키 값을 허용되지 않는다.

맵의 선언

맵을 선언할때는 2개의 타입(키-값)이 필요하다.
이는 같을수도 있고 다를수도 있다.

맵의 다양한 선언 방법

#include <iostream>
#include <map>
#include <vector>
using namespace std;

int main() {
    // 1. 기본적인 map 선언 (키: string, 값: int)
    map<string, int> studentScores;

    // 2. map 선언과 동시에 초기화 (키: int, 값: string)
    map<int, string> idToName = {
        {1, "Alice"},
        {2, "Bob"},
        {3, "Charlie"}
    };

    // 3. map 선언 (키: char, 값: double)
    map<char, double> gradeToGPA;

    // 4. map 선언 (키: int, 값: vector<int>)
    map<int, vector<int>> studentMarks;

    // 5. map 선언 (키: pair<int, int>, 값: string)
    map<pair<int, int>, string> coordinateToName;

    return 0;
}

기존맵을 다른맵에 복사하는 방법

#include <iostream>
#include <map>

using namespace std;

int main() {
    // 원본 map
    map<int, string> originalMap;
    originalMap[1] = "Apple";
    originalMap[2] = "Banana";
    originalMap[3] = "Cherry";

    // 1. 복사 생성자 사용
    map<int, string> copiedMap1(originalMap);

    // 2. 대입 연산자 사용
    map<int, string> copiedMap2;
    copiedMap2 = originalMap;

    // 3. insert 사용
    map<int, string> copiedMap3;
    copiedMap3.insert(originalMap.begin(), originalMap.end());

    return 0;
}

맵의 동작

1️⃣ map은 key순으로 자동 정렬 된다. 이것은 사용자가 특별히 제어하지 않아도 자동으로 계속해서 정렬된다고 보면 된다.
아래는 예시이다.

#include <iostream>
#include <map>

using namespace std;

int main() {
    // map 선언 및 값 추가
    map<int, string> myMap;
    myMap[5] = "E";
    myMap[2] = "B";
    myMap[8] = "H";
    myMap[1] = "A";
    myMap[3] = "C";

    // map 내용 출력
    cout << "Map의 내용 (키 값 기준으로 자동 정렬):" << endl;
    for (map<int, string>::iterator it = myMap.begin(); it != myMap.end(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }

    return 0;
}

2️⃣ insert
make_pair를 통해 키-값 쌍을 만들고 이를 insert를 만들어서 추가할 수 있다.
아래는 예시이다.

#include <iostream>
#include <map>

using namespace std;

int main() {
    map<int, string> myMap;

    // insert를 사용하여 키-값 삽입
    myMap.insert(make_pair(1, "Apple"));
    myMap.insert(make_pair(2, "Banana"));
    myMap.insert(make_pair(3, "Cherry"));

    // map 출력
    for (map<int, string>::iterator it = myMap.begin(); it != myMap.end(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }

    return 0;
}

3️⃣ find
find 함수를 통해 특정 키 값 존재여부를 확인할 수 있다.
아래는 예시이다.

#include <iostream>
#include <map>

using namespace std;

int main() {
    map<int, string> myMap;
    myMap[1] = "Apple";
    myMap[2] = "Banana";

    // 요소 검색
    map<int, string>::iterator it = myMap.find(2);
    if (it != myMap.end()) {
        cout << "Found: " << it->first << ": " << it->second << endl;
    } else {
        cout << "Key not found!" << endl;
    }

    return 0;
}

4️⃣size
맵에 키-값 쌍의 개수를 반환하는 메서드 이다.
아래는 예시이다.

#include <iostream>
#include <map>

using namespace std;

int main() {
    map<int, string> myMap;
    myMap[1] = "Apple";
    myMap[2] = "Banana";

    // map의 크기 출력
    cout << "Map size: " << myMap.size() << endl;

    return 0;
}

5️⃣ clear
맵에 있는 모든 원소를 삭제하려고 할 때 사용하는 메서드 이다. clear는 맵 뿐 아니라 모든 컨테이너에 존재한다.
아래는 예시이다.

#include <iostream>
#include <map>

using namespace std;

int main() {
    map<int, string> myMap;
    myMap[1] = "Apple";
    myMap[2] = "Banana";

    // 모든 요소 삭제
    myMap.clear();

    // map의 크기 출력
    cout << "Map size after clear: " << myMap.size() << endl;

    return 0;
}

알고리즘

STL은 컨테이너에서 할 수 있는 대부분의 동작들을 이미 알고리즘으로 제공하고 있다. 예를 들어 특정 원소 값을 찾거나, 정렬을 할 수 있다.
더욱 좋은 점은 어떤 컨테이너에 대해서도 내부 구현을 알 필요없이 동일한 문법으로 구현할 수 있다.

sort

컨테이너 내부의 데이터를 정렬해주는 함수이다.
기본 타입의 경우 정렬기준을 정해주지 않을 경우 오름차순으로 정렬되며,
사용자가 정렬기준을 정의해서 사용할 수 있다.
정렬의 기준을 제시해야 하는 경우 함수를 추가 작성하는데 이건 아래만 기억하면 된다.
1️⃣ 정렬기준 함수가 true를 반환 하면 순서를 유지한다
2️⃣ 두개의 인자를 받을 떄 앞의 인자가 앞의 원소 뒤의 인자가 뒤의 원소이다.

sort를 활용해서 기본타입 배열을 정렬하는 예(정렬 기준 없음)

#include <iostream>
#include <algorithm> // sort 함수 포함
using namespace std;

int main() {
    int arr[] = {5, 2, 9, 1, 5, 6};
    int size = sizeof(arr) / sizeof(arr[0]);

    // 오름차순 정렬
    sort(arr, arr + size);

    // 결과 출력
    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";
    }
    return 0;
}

sort를 활용해서 기본타입 배열을 정렬하는 예(정렬 기준 있음)

#include <iostream>
#include <algorithm> // sort 함수 포함
using namespace std;

bool compare(int a, int b) {
    return a > b; // 내림차순
}

int main() {
    int arr[] = {5, 2, 9, 1, 5, 6};
    int size = sizeof(arr) / sizeof(arr[0]);

    // 내림차순 정렬
    sort(arr, arr + size, compare);

    // 결과 출력
    for (int i = 0; i < size; i++) {
        cout << arr[i] << " ";
    }
    return 0;
}

sort를 활용해서 기본타입 벡터를 정렬하는 예(정렬 기준 없음)

#include <iostream>
#include <vector>
#include <algorithm> // sort 함수 포함
using namespace std;

int main() {
    vector<int> vec = {5, 2, 9, 1, 5, 6};

    // 오름차순 정렬
    sort(vec.begin(), vec.end());

    // 결과 출력
    for (int num : vec) {
        cout << num << " ";
    }
    return 0;
}

sort를 활용해서 기본타입 벡터를 정렬하는 예(정렬 기준 있음)

#include <iostream>
#include <vector>
#include <algorithm> // sort 함수 포함
using namespace std;

bool compare(int a, int b) {
    return a > b; // 내림차순
}

int main() {
    vector<int> vec = {5, 2, 9, 1, 5, 6};

    // 내림차순 정렬
    sort(vec.begin(), vec.end(), compare);

    // 결과 출력
    for (int num : vec) {
        cout << num << " ";
    }
    return 0;
}

반복자

지금까지 컨테이너와 알고리즘에 대해 알아봤다.
컨테이너의 내부구조는 매우 다르지만, 우리는 대부분 알고리즘을 동일한 코드를 활용해서 사용할 수 있었다.
즉 컨테이너 구현에 의존하지 않고 알고리즘을 활용하는데 전혀 지장이 없었다.

순방향 반복자

순방향 반복자는 앞에 원소부터 뒤에 원소순서로 순회하는 반복자이다.
맨 처음 위치는 begin(), 맨 마지막 다음 위치는 end()가 된다.
맨 마지막 다음위치를 end()로 정한 이유는, 알고리즘 구현의 효율성을 위함이다.
예를 들어 맨 마지막 원소까지 탐색했는데 찾지 못한 경우를 나타내야 하는 경우 end()를 반환해야 한다.

벡터에서 순방향 반복자를 사용하는 예시

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // 순방향 반복자를 사용해 짝수만 출력
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        if (*it % 2 == 0) {
            cout << *it << " ";
        }
    }
    return 0;
}
// 출력: 2 4 6 8 10

여기서 C++ 에서 auto는 자동 타입 추론 키워드이다. 컴파일러가 변수의 타입을 코드에서 할당된 값이나 표현식에 기반해 자동으로 결정한다. auto는 특히 복잡한 타입을 명시하기 어려운 경우에 유용하다 위 코드에서는 반복자(iterator)의 타입을 자동으로 추론한다.
여기서 보통 for문을 쓸때는 후위 연산자 즉 it++와 같이 썼을텐데 왜 STL반복자는 전위 연산자 ++it을 쓸까
우선 it++와 ++it결과는 모두 동일하다.
반복자 it는 증가하고, 루프는 동일하게 작동한다. 따라서 작은 규모의 컨테이너나 단순한 상황에서는 it++을 써도 동작에는 문제가 없다.
차이의 핵심은 성능과 효율성
t++과 ++it 모두 반복자를 증가시키지만, 내부적으로 동작 방식이 다르기 때문에 큰 규모의 데이터나 복잡한 객체를 다룰 때 성능 차이가 생길 수 있다.
작은 컨테이너나 기본 자료형

std::vector<int>

와 같은 간단한 컨테이너
이 경우, 임시 객체 생성이 거의 비용이 들지 않으므로 it++이나 ++it 모두 큰 차이가 없다.
복잡한 컨테이너
예: std::list, std::map 같은 컨테이너의 반복자
it++은 임시 객체를 생성하기 때문에 작은 성능 손실이 발생할 수 있다.
예제
it++ 사용

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    for (auto it = vec.begin(); it != vec.end(); it++) {
        std::cout << *it << " ";
    }
    return 0;
}
1 2 3 4 5

++it 사용

#include <iostream>
#include <vector>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " ";
    }
    return 0;
}
1 2 3 4 5

왜 STL 반복자와 기본 자료형에서 차이가 나는가?
기본 자료형 (int)
i++은 단순히 값을 증가시키는 연산으로, 임시 객체 생성이나 추가 작업이 필요하지 않다.
따라서 i++과 ++i는 동일한 성능을 가지며, 둘 중 무엇을 사용해도 상관없다.
STL 반복자
STL 반복자는 클래스 객체로, it++은 복사 생성자를 호출하여 임시 객체를 생성한다.
이에 비해 ++it은 임시 객체 없이 바로 값을 증가시키므로 더 효율적이다.
기본 자료형에서 ++i를 굳이 사용할 필요는 없는 이유
for (int i = 0; i < vec.size(); i++)에서:
•i++: 후위 증가. i를 증가시키기 전에 기존 값을 사용하지만, 임시 객체가 생성되지 않는다.
•++i: 전위 증가. i를 증가시키고 그 값을 반환한다.
둘 다 컴파일러 최적화에 의해 동일한 머신 코드로 변환된다. 따라서 성능 차이는 없다고 봐도 무방하다.
그래도 ++i를 사용해야 하나?
일반적으로 ++i를 사용하는 것이 STL 반복자에서는 좋은 습관이지만, 기본 자료형에서는 큰 의미가 없다.
다만, 일관성을 위해 ++i를 사용하는 습관을 들이는 것도 괜찮다.
결론
1.기본 자료형에서는 i++과 ++i의 차이가 없으므로 둘 다 사용해도 괜찮다.
•일반적으로는 i++을 더 자주 사용한다.
2.반복자나 사용자 정의 객체를 다룰 때는 ++i를 사용하는 것이 더 효율적이다.
•STL 반복자를 사용할 때는 가급적 ++i를 쓰는 습관을 들이자.
3.코드 스타일에 맞춰 통일성 있게 사용하는 것이 가장 중요하다.

맵에서 순방향 반복자를 사용하는 예시

#include <map>
#include <iostream>
using namespace std;

int main() {
    map<string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Charlie", 88}};
    
    // 순방향 반복자를 사용해 맵의 키-값 쌍 출력
    for (auto it = scores.begin(); it != scores.end(); ++it) {
        cout << it->first << ": " << it->second << endl;
    }
    return 0;
}
// 출력:
// Alice: 90
// Bob: 85
// Charlie: 88

문자열에서 순방향 반복자를 사용하는 예시

#include <vector>
#include <string>
#include <algorithm>
#include <iostream>
using namespace std;

int main() {
    vector<string> words = {"apple", "banana", "cherry", "date"};

    string target = "cherry";

    auto it = find(words.begin(), words.end(), target);

    if (it != words.end()) {
        cout << "Word \"" << target << "\" found at index " << distance(words.begin(), it) << endl;
    } else {
        cout << "Word \"" << target << "\" not found." << endl;
    }

    return 0;
}
// 출력: Word "cherry" found at index 2

역방향 반복자

역방향 반복자는 앞에원소부터 뒤에 원소대로 순회하는 반복자이다.
맨 마지막 위치는 rbegin(), 맨 처음 이전위치는 rend()가 된다

예를 들어 맨 처음 원소까지 탐색했는데 찾지 못한 경우를 나타내야 하는 경우 rend()를 반환한다.

벡터에서 역방향 반복자를 사용한 예시

#include <vector>
#include <iostream>
using namespace std;

int main() {
    vector<int> numbers = {10, 15, 20, 25, 30};

    // 역방향 반복자로 짝수만 출력
    for (auto it = numbers.rbegin(); it != numbers.rend(); ++it) {
        if (*it % 2 == 0) {
            cout << *it << " ";
        }
    }

    return 0;
}
// 출력: 30 20 10

맵에서 역방향 반복자를 사용한 예시

#include <map>
#include <iostream>
using namespace std;

int main() {
    map<string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Charlie", 88}};

    // 역방향 반복자로 값이 88 이상인 항목만 출력
    for (auto it = scores.rbegin(); it != scores.rend(); ++it) {
        if (it->second >= 88) {
            cout << it->first << ": " << it->second << endl;
        }
    }

    return 0;
}
// 출력:
// Charlie: 88
// Alice: 90

문자열에서 역방향 반복자를 사용한 예시

#include <vector>
#include <string>
#include <algorithm>
#include <iostream>
using namespace std;

int main() {
    vector<string> words = {"apple", "banana", "cherry", "date"};

    string target = "banana";

    auto it = find(words.rbegin(), words.rend(), target);

    if (it != words.rend()) {
        cout << "Word \"" << target << "\" found at reverse index " 
             << distance(words.rbegin(), it) << " (from the back)" << endl;
        cout << "Word \"" << target << "\" found at forward index " 
             << distance(words.begin(), it.base()) - 1 << " (from the front)" << endl;
    } else {
        cout << "Word \"" << target << "\" not found." << endl;
    }

    return 0;
}
// 출력:
// Word "banana" found at reverse index 2 (from the back)
// Word "banana" found at forward index 1 (from the front)

반복자를 활용하여 각 컨테이너를 순회하기

한번 문제 부분을 채워보자.

#include <iostream>
#include <vector>
#include <map>

using namespace std;

int main() {
    // 벡터와 맵 데이터 정의
    vector<int> vec = { 10, 20, 30, 40, 50 };
    map<string, int> mp = {
        {"Alice", 90},
        {"Bob", 85},
        {"Charlie", 95}
    };

    // 문제: 아래 부분을 완성하세요

    return 0;
}

정답

#include <iostream>
#include <vector>
#include <map>

using namespace std;

int main() {
    // 벡터와 맵 데이터 정의
    vector<int> vec = { 10, 20, 30, 40, 50 };
    map<string, int> mp = {
        {"Alice", 90},
        {"Bob", 85},
        {"Charlie", 95}
    };
    cout << "Vector:\n";
    cout << "[Forward]: ";
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        cout << *it << " ";
    }
    cout << "[Backward]: ";
    for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
        cout << *rit << " ";
    }
    cout << endl;
    cout << "Map:\n";
    cout << "[Forward]: ";
    for (auto it = mp.begin(); it != mp.end(); ++it) {
        cout << "(" << it->first << ", " << it->second << ")";
    }
    cout << endl;
    cout << "[Backward]: ";
    for (auto rit = mp.rbegin(); rit != mp.rend(); ++rit) {
        cout << "(" << rit->first << ", " << rit->second << ")";
    }
    cout << endl;
    return 0;
}   

0개의 댓글