본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : std::vector (98, 11, 14, 17, 20)

by snowoods 2025. 8. 21.

Modern C++

C++ std::vector 컨테이너

 

개요

std::vector는 C++ 표준 라이브러리에서 제공하는 동적 배열 컨테이너로, 연속된 메모리 공간에 요소를 저장합니다. 자동으로 메모리를 관리하고 크기를 동적으로 조정할 수 있어 가장 널리 사용되는 컨테이너 중 하나입니다. 임의 접근이 가능하고, 끝에서의 삽입/삭제가 효율적입니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++98/03: 기본 std::vector 기능 도입
  • C++11: 이동 생성자/대입 연산자, emplace_back(), shrink_to_fit()
  • C++14: 일반화된 람다 캡처와 함께 사용 개선
  • C++17: 구조적 바인딩과 함께 사용 개선, emplace_back()이 참조 반환
  • C++20: std::span과의 호환성, std::ranges 지원

 

내용 설명

주요 특징

  1. 동적 배열: 연속된 메모리 공간에 요소 저장
  2. 자동 크기 조정: 필요에 따라 메모리를 자동으로 재할당
  3. 임의 접근: 인덱스를 통한 O(1) 시간 접근
  4. 끝에서의 효율적 연산: push_back(), pop_back()은 분할 상환 O(1) 시간
  5. 중간 삽입/삭제: O(n) 시간이 소요됨

 

주요 멤버 함수

  • size(): 요소 개수 반환
  • capacity(): 할당된 메모리 공간의 크기 반환
  • empty(): 컨테이너가 비었는지 확인
  • push_back(): 끝에 요소 추가
  • pop_back(): 마지막 요소 제거
  • insert(): 지정된 위치에 요소 삽입
  • erase(): 지정된 위치의 요소 제거
  • clear(): 모든 요소 제거
  • reserve(): 용량 미리 할당
  • shrink_to_fit(): 용량을 현재 크기에 맞춤

 

예제 코드

#include <cstdint>
#include <iostream>
#include <vector>

int main()
{
    // 크기 0으로 벡터 생성
    auto my_vec_empty = std::vector<std::int32_t>{};

    // 초기값을 가진 벡터 생성 (크기 5)
    auto my_vec = std::vector<std::int32_t>{1, 2, 3, 4, 5};

    // C 스타일 for 루프로 요소 접근
    std::cout << "\nC-Style Loop: \n";
    for (std::size_t i = 0; i < my_vec.size(); i++)
    {
        std::cout << my_vec[i] << '\n';
    }

    // 범위 기반 for 루프 (읽기 전용)
    std::cout << "\nC++ Ranged For Loop (read-only): \n";
    for (const auto value : my_vec)
    {
        std::cout << value << '\n';
    }

    // 범위 기반 for 루프 (값 수정)
    for (auto &value : my_vec)
    {
        value *= 2; // 모든 요소를 2배로
    }

    std::cout << "\nAfter doubling all values:\n";
    for (const auto value : my_vec)
    {
        std::cout << value << '\n';
    }

    // 크기 3의 벡터를 0으로 초기화하여 생성
    auto my_vec2 = std::vector<std::int32_t>(3, 0);
    std::cout << "\nVector with 3 zeros: \n";
    for (const auto value : my_vec2)
    {
        std::cout << value << '\n';
    }

    // 요소 추가 및 제거
    auto my_vec3 = std::vector<std::int32_t>{};
    my_vec3.push_back(10);
    my_vec3.push_back(22);
    std::cout << "\nAfter push_back(10) and push_back(22): \n";
    for (const auto value : my_vec3)
    {
        std::cout << value << '\n';
    }

    my_vec3.pop_back();
    std::cout << "\nAfter pop_back(): \n";
    for (const auto value : my_vec3)
    {
        std::cout << value << '\n';
    }

    // 반복자 사용
    auto it_begin = my_vec2.begin(); // 첫 번째 요소를 가리키는 반복자
    auto it_end = my_vec2.end();     // 마지막 요소 다음을 가리키는 반복자

    std::cout << "\nUsing Iterators: \n";
    for (; it_begin != it_end; ++it_begin)
    {
        std::cout << *it_begin << '\n';
    }

    // 요소 삽입
    my_vec2.insert(my_vec2.begin() + 1, 100);
    std::cout << "\nAfter inserting 100 at index 1: \n";
    for (const auto value : my_vec2)
    {
        std::cout << value << '\n';
    }

    return 0;
}

 

실행 결과

C-Style Loop: 
1
2
3
4
5

C++ Ranged For Loop (read-only): 
1
2
3
4
5

After doubling all values:
2
4
6
8
10

Vector with 3 zeros: 
0
0
0

After push_back(10) and push_back(22): 
10
22

After pop_back(): 
10

Using Iterators: 
0
0
0

After inserting 100 at index 1: 
0
100
0
0

예제 코드

C++20 이전 중간 요소 삭제.

 - std::remove_if와 erase 사용 (Erase-Remove Idiom)

 - std::remove_if: 삭제할 요소들을 벡터의 뒤쪽으로 옮기고, 삭제되지 않을 요소들의 새로운 끝 위치를 가리키는 반복자를 반환합니다. 벡터의 실제 크기는 변하지 않습니다.

 - vector::erase: remove_if가 반환한 위치부터 벡터의 실제 끝까지의 요소들을 물리적으로 제거하여 벡터의 크기를 줄입니다.

#include <iostream>
#include <vector>
#include <algorithm> // std::remove_if를 위해 필요

void print_vector(const std::string& title, const std::vector<int>& vec) {
    std::cout << title;
    for (const auto& val : vec) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::cout << "--- 예제 1: C++20 이전 (std::remove_if + erase) ---\n";
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    print_vector("삭제 전: ", numbers);

    // 1단계: 짝수인 요소들을 뒤로 보내고, 남길 요소들의 새로운 끝을 찾음
    auto new_end = std::remove_if(numbers.begin(), numbers.end(), [](int value) {
        return value % 2 == 0; // 짝수이면 true를 반환 -> 삭제 대상
    });

    // remove_if 실행 후 벡터 상태 (크기는 아직 10)
    // numbers는 {1, 3, 5, 7, 9, [쓰레기값], [쓰레기값], ...} 형태가 됨
    print_vector("remove_if 후 (삭제 전): ", numbers);
    std::cout << "벡터의 실제 크기: " << numbers.size() << std::endl;

    // 2단계: new_end부터 실제 끝까지의 요소들을 물리적으로 삭제
    numbers.erase(new_end, numbers.end());

    print_vector("최종 결과: ", numbers);
    std::cout << "벡터의 최종 크기: " << numbers.size() << std::endl;

    return 0;
}

 

실행 결과

--- 예제 1: C++20 이전 (std::remove_if + erase) ---
삭제 전: 1 2 3 4 5 6 7 8 9 10 
remove_if 후 (삭제 전): 1 3 5 7 9 10 7 8 9 10 
벡터의 실제 크기: 10
최종 결과: 1 3 5 7 9 
벡터의 최종 크기: 5

(참고: remove_if 후 뒤쪽에 남는 값들은 유효하지만 어떤 값이 될지는 정해져 있지 않습니다. 위 결과의 10 7 8 9 10은 컴파일러나 환경에 따라 다를 수 있습니다.)


 예제 코드

C++20에서 중간 요소 삭제.

위 두 단계의 과정을 하나의 함수 호출로 깔끔하게 처리할 수 있습니다. (erase_if)

#include <iostream>
#include <vector>
// #include <algorithm>는 <vector>에 포함되는 경우가 많지만 명시적으로 적는 것이 좋음

void print_vector(const std::string& title, const std::vector<int>& vec) {
    std::cout << title;
    for (const auto& val : vec) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::cout << "\n--- 예제 2: C++20 (std::erase_if) ---\n";
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    print_vector("삭제 전: ", numbers);
    std::cout << "벡터의 초기 크기: " << numbers.size() << std::endl;

    // 한 번의 호출로 조건에 맞는 요소를 모두 삭제
    std::erase_if(numbers, [](int value) {
        return value % 2 == 0; // 짝수이면 true를 반환 -> 삭제 대상
    });

    print_vector("최종 결과: ", numbers);
    std::cout << "벡터의 최종 크기: " << numbers.size() << std::endl;

    return 0;
}

 

실행 결과

--- 예제 2: C++20 (std::erase_if) ---
삭제 전: 1 2 3 4 5 6 7 8 9 10 
벡터의 초기 크기: 10
최종 결과: 1 3 5 7 9 
벡터의 최종 크기: 5

 

활용팁

  1. 사전 할당: 많은 요소를 삽입할 계획이라면 reserve()로 메모리를 미리 할당하면 재할당 오버헤드를 줄일 수 있습니다.
  2. 요소 접근: at()은 범위 검사를 수행하지만, operator[]는 범위 검사를 하지 않아 더 빠릅니다.
  3. 반복자 무효화: 요소를 추가하거나 제거하면 반복자와 참조가 무효화될 수 있으니 주의가 필요합니다.
  4. 용량 관리: shrink_to_fit()을 사용하여 불필요한 메모리를 해제할 수 있지만, 재할당이 발생할 수 있습니다.
  5. 임시 객체: emplace_back()을 사용하면 임시 객체 생성을 피할 수 있어 더 효율적입니다.
  6. 데이터 연속성: 연속된 메모리 공간을 사용하므로 C 스타일 배열과의 호환성이 좋습니다.
  7. 크기 vs 용량: size()는 실제 요소 수를, capacity()는 할당된 메모리 공간을 반환합니다.
  8. 예외 안전: 대부분의 연산이 강력한 예외 안전성(strong exception safety)을 보장합니다.