C++ 벡터 컨테이너 (std::vector)
한눈에 보기
연속 메모리 기반 동적 배열인 std::vector의 size와 capacity, 재할당과 분할 상환 O(1) push_back, 반복자 무효화, reserve와 emplace_back 활용법을 정리한다.
최종 업데이트 2026.09.17
std::vector는 C++ 표준 라이브러리가 제공하는 동적 배열(Dynamic Array) 컨테이너로, 원소를 힙(Heap)의 연속된 메모리에 저장하면서 크기가 자동으로 늘어난다. 인덱스 접근이 O(1)이고 끝에 추가하는 push_back이 분할 상환(Amortized) O(1)이라는 성능 특성을 가지며, 크기(size)와 용량(capacity)의 구분, 재할당(Reallocation)과 반복자 무효화(Iterator Invalidation)를 이해하는 것이 활용의 핵심이다.
내부 구조: size와 capacity
vector는 개념적으로 세 가지 정보를 가진다. 힙에 할당한 배열의 시작 주소, 현재 담긴 원소 개수인 size, 그리고 재할당 없이 담을 수 있는 최대 개수인 capacity다. size가 capacity에 도달한 상태에서 push_back을 호출하면, 더 큰 새 배열을 할당하고 기존 원소를 모두 옮긴 뒤 옛 배열을 해제하는 재할당이 일어난다. 이때 성장 배율은 구현마다 다르지만 보통 1.5배나 2배씩 늘린다. 매번 1칸씩 늘리면 추가마다 전체 복사가 필요해 O(n)이 되지만, 배수로 늘리면 재할당 비용이 여러 번의 추가에 분산되어 push_back의 평균 비용이 O(1)이 된다. 이것이 분할 상환 분석의 대표 사례다.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
for (int i = 0; i < 10; i++) {
v.push_back(i);
std::cout << v.size() << " / " << v.capacity() << '\n';
}
/* size는 1씩 늘지만 capacity는 1, 2, 4, 8, 16처럼 배수로 는다 */
return 0;
}
최종 원소 개수를 미리 안다면 reserve(n)으로 용량을 확보해 재할당과 복사를 아예 없앨 수 있다. resize(n)은 용량이 아니라 size 자체를 바꿔 원소를 실제로 만들거나 지운다는 점에서 reserve와 다르다. 원소를 지워도 capacity는 줄지 않으므로, 메모리를 돌려주고 싶으면 shrink_to_fit을 호출하거나 빈 vector와 swap하는 관용구를 쓴다.
주요 연산과 시간 복잡도
| 연산 | 복잡도 | 비고 |
|---|---|---|
| 인덱스 접근 v[i], at(i) | O(1) | at은 범위 검사 후 예외 발생 |
| push_back, pop_back | 분할 상환 O(1) | 재할당 시에만 O(n) |
| 중간 insert, erase | O(n) | 뒤쪽 원소를 모두 이동 |
| 탐색(정렬 안 된 상태) | O(n) | 정렬 후 이분 탐색은 O(log n) |
연속 메모리라는 특성 덕분에 vector는 캐시 지역성(Cache Locality)이 뛰어나다. 순회할 때 인접 원소가 이미 캐시에 올라와 있으므로, 노드가 힙에 흩어져 있는 std::list보다 실제 순회 속도가 훨씬 빠르다. 중간 삽입과 삭제가 이론상 O(1)인 list가 실측에서는 vector에 지는 경우가 많은 이유이기도 하다. 이런 이유로 모던 C++(Modern C++) 진영에서는 특별한 근거가 없는 한 기본 컨테이너로 vector를 쓰라고 권장한다.
반복자 무효화
재할당이 일어나면 원소들이 새 메모리로 이동하므로, 이전 배열을 가리키던 모든 반복자(Iterator), 포인터, 참조가 무효가 된다. 재할당이 없더라도 중간 erase나 insert는 해당 위치 뒤쪽의 반복자를 무효화한다. 반복문을 돌면서 원소를 지울 때 흔히 실수하는 지점이며, erase가 반환하는 다음 위치 반복자를 받아서 진행하는 것이 올바른 패턴이다.
std::vector<int> v = {1, 2, 3, 4, 5, 6};
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
it = v.erase(it); /* 지운 다음 위치를 돌려받는다 */
} else {
++it;
}
}
/* C++20부터는 std::erase_if(v, 조건)로 한 줄에 처리할 수 있다 */
사용할 때 알아 두면 좋은 것들
emplace_back은 인자를 받아 vector 내부 공간에서 원소를 직접 생성하므로, 임시 객체를 만들어 넣는 push_back보다 복사나 이동을 줄일 수 있다. 원소 타입의 이동 생성자가 noexcept로 선언되어 있으면 재할당 시 복사 대신 이동이 사용되어 성능이 좋아진다는 점도 모던 C++에서 중요한 포인트다. vector가 스스로 힙 메모리를 할당하고 소멸 시 해제하는 것은 자원 획득은 초기화(RAII, Resource Acquisition Is Initialization) 원칙의 전형이므로, new로 만든 동적 배열을 직접 delete하는 코드 대부분은 vector로 대체하는 것이 안전하다. 한 가지 예외적인 주의 사항으로 vector<bool>은 공간 절약을 위해 비트 단위로 압축된 특수화라서 원소의 실제 참조를 얻을 수 없으므로, 불리언 배열이 필요하면 std::deque<bool>이나 std::vector<char>를 고려한다.
면접에서 이렇게 물어본다
- vector의 size와 capacity의 차이는 무엇이며, capacity는 언제 어떻게 늘어나는가?
- push_back이 분할 상환 O(1)인 이유를 재할당 전략과 함께 설명해 보라.
- 반복자 무효화란 무엇이고, 순회 중 원소를 삭제할 때 어떻게 처리해야 하는가?
- vector와 list의 차이를 캐시 지역성 관점까지 포함해 비교해 보라.
- reserve와 resize의 차이, 그리고 push_back과 emplace_back의 차이는 무엇인가?
C / C++에서 같이 읽기
- C 언어 컴파일 과정 (Compile Process)C 소스 코드가 전처리, 컴파일, 어셈블, 링킹 네 단계를 거쳐 실행 파일이 되는 과정을 단계별 산출물, 목적 파일의 섹션 구조, 정적 링킹과 동적 링킹의 차이와 함께 정리한다.
- C 동적 할당 (malloc, free)실행 중에 힙 메모리를 확보하고 반납하는 C의 동적 할당을 malloc, calloc, realloc, free의 동작과 할당기의 내부 구조, 메모리 누수와 댕글링 포인터 같은 대표 오류 중심으로 설명한다.
- 포인터 (Pointer)다른 변수의 메모리 주소를 저장하는 포인터의 개념과 역참조, 포인터 산술, 배열과의 관계, 이중 포인터와 함수 포인터, 그리고 널 포인터와 댕글링 포인터 같은 오류 상황까지 정리한다.
- 구조체 메모리 크기와 정렬, 패딩 (Struct Alignment)구조체 크기가 멤버 합보다 커지는 이유인 메모리 정렬과 패딩의 규칙을 오프셋 계산 예시로 설명하고, 멤버 순서에 따른 크기 차이와 #pragma pack 사용 시 주의점을 다룬다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기