[C++ STL] vector 완벽 정리: 기본부터 심화까지 (면접 대비)

Date:     Updated:

카테고리:

태그:

1. 📚 vector의 개념

C++ STL(Standard Template Library)에서 가장 자주 사용되는 컨테이너 중 하나인 std::vector는 동적 배열의 역할을 하며, 그 유연성과 효율성 덕분에 다양한 상황에서 활용됩니다.

📌 vector의 특징

  • 연속적인 메모리 공간: 같은 타입의 요소를 연속적인 메모리 공간에 저장하는 동적 배열(dynamic array)
  • 동적 크기 조절: C 스타일 배열과 달리, 크기가 프로그램 실행 중에 동적으로 조절될 수 있음
  • 효율적인 접근: 인덱스를 사용한 요소 접근(O(1))과 순차적인 순회 성능이 뛰어남
  • 자동 메모리 관리: 요소 삽입 또는 삭제 시 필요에 따라 자동으로 크기를 조절

2. 💻 vector의 기본 사용법

📋 vector 선언 및 초기화

  • 기본 선언:

    std::vector<T> vector_name;  // T는 요소의 타입
    
  • 초기화 리스트 사용:

    std::vector<int> v = {1, 2, 3, 4, 5};
    
  • 단일 값으로 초기화:

    std::vector<int> v(5, 11);  // 크기 5, 모든 요소를 11로 초기화
    
  • 다른 컨테이너의 범위에서 초기화:

    int arr[] = {1, 2, 3, 4, 5};
    std::vector<int> v(arr, arr + 5);  // 배열에서 초기화
    
    std::vector<int> v1 = {1, 2, 3, 4, 5};
    std::vector<int> v2(v1.begin(), v1.end());  // 다른 벡터에서 초기화
    
  • std::fill() 함수 사용:

    std::vector<int> v(5);
    std::fill(v.begin(), v.end(), 11);
    
  • std::iota() 함수 사용:

    std::vector<int> v(5);
    std::iota(v.begin(), v.end(), 11);  // 11, 12, 13, 14, 15
    

🔧 요소 추가 및 삭제

  • 끝에 요소 추가:

    v.push_back(6);  // 끝에 요소 추가
    v.emplace_back(7);  // 끝에 요소를 직접 생성하여 추가
    
  • 중간에 요소 삽입:

    v.insert(v.begin() + 1, 10);  // 인덱스 1에 10 삽입
    v.emplace(v.begin() + 1, 10);  // 인덱스 1에 10을 직접 생성하여 삽입
    
  • 끝에서 요소 삭제:

    v.pop_back();  // 마지막 요소 제거
    
  • 특정 위치 또는 범위 요소 삭제:

    v.erase(v.begin() + 1);  // 인덱스 1의 요소 제거
    v.erase(v.begin() + 1, v.begin() + 3);  // 인덱스 1부터 3 전까지의 요소 제거
    

🔍 요소 접근 및 수정

  • 인덱스 접근:

    int value = v[0];  // 인덱스 0의 요소 접근 (경계 검사 없음)
    int safe_value = v.at(0);  // 인덱스 0의 요소 접근 (경계 검사 있음)
    
  • 요소 값 변경:

    v[0] = 100;  // 인덱스 0의 요소 값을 100으로 변경
    v.at(0) = 100;  // 인덱스 0의 요소 값을 100으로 변경
    
  • 첫 번째/마지막 요소 접근:

    int first = v.front();  // 첫 번째 요소
    int last = v.back();  // 마지막 요소
    

3. ⚡ vector의 내부 작동 및 성능

📊 크기(size)와 용량(capacity)

  • size(): 벡터에 현재 존재하는 요소의 개수
  • capacity(): 벡터에 현재 할당된 메모리 공간에 저장할 수 있는 최대 요소 개수
  • 차이점: size는 실제 요소 수이고 capacity는 할당된 메모리 크기 (일반적으로 capacity >= size)

🔄 동적 재할당

  • 요소를 추가하다가 현재 용량이 부족해지면 자동으로 더 큰 메모리 블록을 할당하고 기존 요소를 새 위치로 복사/이동
  • push_back은 평균적으로 O(1)이지만, 재할당이 발생할 때는 O(n)
  • 잦은 재할당을 피하기 위해 reserve()를 사용하여 미리 충분한 용량을 확보하는 것이 좋음

🚀 성능 최적화

  • reserve() 사용:

    std::vector<int> v;
    v.reserve(1000);  // 최소 1000개의 요소를 저장할 수 있도록 용량 예약
    
  • 범위 기반 생성자/삽입 사용:

    std::vector<int> v(first, last);  // 범위 기반 생성자
    v.insert(v.end(), first, last);  // 범위 기반 삽입
    

4. 🛠️ vector의 주요 멤버 함수

함수 설명 시간 복잡도
empty() 벡터가 비어있는지 확인 O(1)
size() 벡터의 현재 크기 반환 O(1)
capacity() 벡터의 현재 용량 반환 O(1)
reserve(n) 최소 n개의 요소를 저장할 수 있도록 용량 예약 O(n)
resize(n) 벡터의 크기를 n으로 변경 O(n)
clear() 모든 요소를 제거하고 벡터를 비움 O(n)
shrink_to_fit() 벡터의 용량을 현재 크기에 맞게 줄임 O(n)
swap(other_vector) 다른 벡터와 내용을 맞바꿈 O(1)
data() 벡터 내부 배열에 대한 포인터 반환 O(1)
max_size() 벡터가 보유할 수 있는 최대 요소 수 반환 O(1)

5. 🔄 이터레이터(iterator)

이터레이터는 벡터 요소의 메모리 주소를 가리키는 포인터와 유사한 객체로, 벡터의 요소를 순회하는 데 사용됩니다.

📝 이터레이터 사용법

  • 이터레이터 선언:

    std::vector<int>::iterator iter;
    
  • 이터레이터 초기화:

    iter = v.begin();  // 첫 번째 요소를 가리키는 이터레이터
    iter = v.end() - 1;  // 마지막 요소를 가리키는 이터레이터
    
  • 이터레이터를 사용한 순회:

    for (iter = v.begin(); iter != v.end(); ++iter) {
        std::cout << *iter << " ";
    }
    
  • 범위 기반 for 루프:

    for (const auto &element : v) {
        std::cout << element << " ";
    }
    
  • 상수 이터레이터:

    for (auto iter = v.cbegin(); iter != v.cend(); ++iter) {
        std::cout << *iter << " ";
    }
    

6. ⚠️ 주의사항 및 팁

🚨 접근 오류

  • [] 연산자는 경계 검사를 하지 않으므로, 존재하지 않는 요소에 접근 시 미정의 동작을 초래할 수 있음
  • 안전한 접근을 위해 at() 함수 사용 권장

🔄 크기 변경 중 순회

  • 범위 기반 for 루프나 일반 for 루프에서 size()를 사용하면서 벡터의 크기를 변경하면 문제가 발생할 수 있음
  • 순회 중 요소 추가/삭제가 필요하다면 주의해야 함 (예: 루프 진입 전 size 저장 또는 이터레이터 사용 시 유효성 관리)

🧹 메모리 해제

  • clear()는 size만 0으로 만들 뿐, capacity는 유지될 수 있음
  • shrink_to_fit()은 메모리 해제를 요청하지만 표준에 의해 보장되지 않음
  • 메모리를 확실히 해제하는 가장 확실한 방법은 빈 벡터와 swap하는 것:
    std::vector<int> v;
    // ... 벡터 사용 ...
    std::vector<int>().swap(v);  // 메모리 해제
    

⚠️ 이터레이터 무효화

  • 벡터에 요소가 추가되거나 삭제될 때, 특히 재할당이 발생하면 기존 이터레이터, 포인터, 참조가 무효화될 수 있음
  • push_back은 재할당이 발생하지 않는 한 기존 끝 이터레이터만 무효화하지만, 재할당 시 모든 이터레이터/포인터/참조가 무효화됨
  • inserterase는 해당 위치 이후의 모든 이터레이터/포인터/참조를 무효화할 수 있음

7. 💡 코드 구현 예시

📝 기본적인 vector 사용 예시

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

int main() {
    // 벡터 선언 및 초기화
    std::vector<int> numbers = {5, 2, 8, 1, 9, 3};

    // 요소 추가
    numbers.push_back(7);

    // 요소 접근 및 수정
    std::cout << "첫 번째 요소: " << numbers.front() << std::endl;
    std::cout << "마지막 요소: " << numbers.back() << std::endl;

    // 정렬
    std::sort(numbers.begin(), numbers.end());

    // 범위 기반 for 루프로 순회
    std::cout << "정렬된 벡터: ";
    for (const auto &num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    // 벡터 크기 및 용량 출력
    std::cout << "벡터 크기: " << numbers.size() << std::endl;
    std::cout << "벡터 용량: " << numbers.capacity() << std::endl;

    return 0;
}

🔄 이터레이터를 사용한 벡터 조작 예시

#include <iostream>
#include <vector>

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

    // 이터레이터를 사용한 순회
    std::cout << "이터레이터로 순회: ";
    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 이터레이터를 사용한 요소 삽입
    auto it = v.begin() + 2;
    v.insert(it, 10);

    // 이터레이터를 사용한 요소 삭제
    it = v.begin() + 3;
    v.erase(it);

    // 결과 출력
    std::cout << "수정된 벡터: ";
    for (const auto &element : v) {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    return 0;
}

8. 🎯 면접 대비 핵심 개념

📝 기본 개념 및 특징

Q1. std::vector란 무엇인가요? std::array나 일반 C++ 배열과의 차이점은 무엇인가요?

A. std::vector는 C++ STL의 동적 배열 컨테이너입니다.

  • 주요 특징:

    • 동적 크기 조절 가능 (실행 중 크기 변경)
    • 연속적인 메모리 공간에 요소 저장
    • 자동 메모리 관리 (RAII)
    • STL 알고리즘과의 완벽한 호환성
  • vs 일반 배열:

    • 일반 배열: 고정 크기, 스택 할당, 수동 메모리 관리
    • std::vector: 동적 크기, 힙 할당, 자동 메모리 관리
  • vs std::array:

    • std::array: 컴파일 타임에 크기 고정, 스택 할당
    • std::vector: 런타임에 크기 변경 가능, 힙 할당

💡 주요 멤버 함수 및 사용법

Q2. push_back()emplace_back()의 차이점은 무엇인가요?

A. 두 함수 모두 벡터의 끝에 요소를 추가하지만, 동작 방식이 다릅니다.

  • push_back():

    • 객체를 생성한 후 벡터에 복사/이동
    • 임시 객체 생성 → 복사/이동 생성자 호출 → 임시 객체 파괴
    vec.push_back(std::string("Hello"));  // 임시 객체 생성 후 이동
    
  • emplace_back():

    • 벡터 내부에서 직접 객체 생성
    • 생성자 인자만 전달하여 내부에서 직접 생성
    • 불필요한 임시 객체 생성/파괴와 복사/이동 연산 제거
      vec.emplace_back("Hello");  // 내부에서 직접 생성
      

Q3. vector::operator[]vector::at()의 차이점은 무엇인가요?

A. 두 함수는 요소 접근 방식이 다릅니다.

  • operator[]:

    • 범위 검사 없음
    • 더 빠른 접근 속도
    • 잘못된 인덱스 접근 시 정의되지 않은 동작
    • 성능이 중요한 내부 루프에서 사용 권장
  • at():

    • 범위 검사 수행
    • 잘못된 인덱스 접근 시 std::out_of_range 예외 발생
    • 안전성이 중요한 경우 사용 권장

🔧 메모리 관리 및 성능

Q4. size()capacity()의 차이점은 무엇인가요?

A. 두 값은 벡터의 메모리 관리와 관련이 있습니다.

  • size():

    • 현재 저장된 요소의 실제 개수
    • push_back(), pop_back() 등으로 변경
    • 항상 capacity() 이하
  • capacity():

    • 재할당 없이 저장 가능한 최대 요소 개수
    • 재할당 시 일반적으로 2배로 증가
    • reserve()로 미리 설정 가능

Q5. 벡터의 메모리를 해제하는 방법은 무엇인가요?

A. 여러 방법이 있으며, 각각의 특징이 있습니다.

  • clear():

    • 요소만 제거하고 size를 0으로 설정
    • capacity는 유지
  • shrink_to_fit():

    • capacitysize에 맞게 줄이도록 요청
    • 표준상 보장되지 않음(구현에 따라 요청이 충족되지 않을 수 있음)
  • 빈 벡터와 swap:

    std::vector<T>().swap(vec);  // 가장 확실한 메모리 해제 방법
    

⚡ 성능 및 최적화

Q6. reserve()resize()의 차이점은 무엇인가요?

A. 두 함수는 다른 목적으로 사용됩니다.

  • reserve(n):

    • capacity만 변경
    • size는 변경되지 않음
    • 재할당 비용 감소를 위한 최적화
  • resize(n):

    • size를 변경
    • 새로운 요소는 기본값으로 초기화
    • 필요시 capacity도 증가

Q7. 벡터를 순회하는 방법과 주의사항은 무엇인가요?

A. 여러 방법이 있으며, 각각의 장단점이 있습니다.

  • 인덱스 기반 for 루프:

    for (size_t i = 0; i < vec.size(); ++i) {
        // vec[i] 사용
    }
    
  • 범위 기반 for 루프:

    for (const auto& element : vec) {
        // element 사용
    }
    
  • 이터레이터 사용:

    for (auto it = vec.begin(); it != vec.end(); ++it) {
        // *it 사용
    }
    

주의사항:

  • 순회 중 벡터 크기 변경 시 이터레이터 무효화
  • 범위 기반 for 루프에서 크기 변경 시 정의되지 않은 동작
  • 삭제 시 erase()가 반환하는 다음 이터레이터 사용

📊 컨테이너 선택 가이드

Q8. std::vectorstd::list 중 어떤 것을 선택해야 하나요?

A. 사용 사례에 따라 선택이 달라집니다.

  • std::vector 선택 시:

    • 빈번한 랜덤 접근이 필요한 경우
    • 순차적 순회가 주된 작업인 경우
    • 끝에서 주로 추가/삭제하는 경우
    • 메모리 효율성이 중요한 경우
  • std::list 선택 시:

    • 중간 삽입/삭제가 매우 빈번한 경우
    • 랜덤 접근이 거의 없는 경우
    • 요소 순서 유지가 덜 중요한 경우

🎯 추가 면접 팁

  1. 2차원 벡터:

    std::vector<std::vector<int>> matrix;
    
  2. 함수 인자 전달:

    • 값 전달: void func(std::vector<int> vec)
    • 참조 전달: void func(const std::vector<int>& vec)
    • 이동 전달: void func(std::vector<int>&& vec)
  3. 자주 사용되는 멤버 함수:

    • empty(), front(), back()
    • swap(), assign(), data()
    • begin(), end(), rbegin(), rend()

9. 📖 참고 자료

STL 카테고리 내 다른 글 보러가기