C++ vector 기초 완벽 가이드 | 초기화·연산·용량 관리와 실전 패턴
이 글의 핵심
C++ vector 기초 완벽 가이드에 대한 실전 가이드입니다. 초기화·연산·용량 관리와 실전 패턴 등을 예제와 함께 상세히 설명합니다.
들어가며: vector 인덱스 접근에서 segmentation fault
”vec[10]에 접근했는데 프로그램이 죽어요”
std::vector는 C++ STL의 동적 배열입니다. 크기가 자동으로 늘어나고, 연속된 메모리에 저장되어 캐시 친화적이며, 인덱스 접근이 O(1)입니다. 이론·코딩 테스트 관점의 배열·동적 배열은 알고리즘 시리즈: 배열과 리스트와 짝지어 읽기 좋습니다. 하지만 [] 연산자는 범위 검사를 하지 않아 유효한 인덱스를 벗어나면 미정의 동작(segmentation fault 가능)이 발생합니다.
비유하면 vector는 “자동으로 늘어나는 서랍장”입니다. 서랍이 3개인데 10번째 서랍을 열려고 하면 문제가 생깁니다. C 스타일 배열 int arr[100]은 크기가 고정되어 있고, new[]/delete[]는 직접 관리해야 합니다. vector는 RAII로 메모리가 자동 해제되며, size()·capacity()·reserve()로 성능을 제어할 수 있습니다.
문제의 코드:
std::vector<int> vec = {1, 2, 3};
int value = vec[10]; // ❌ size는 3인데 [10] 접근 → 미정의 동작!
위 코드 설명: vec의 size()는 3이므로 유효한 인덱스는 0, 1, 2입니다. vec[10]은 범위를 벗어나 미정의 동작이 되며, 실행 환경에 따라 segmentation fault가 발생할 수 있습니다.
해결법:
// ✅ at() 사용: 범위 벗어나면 std::out_of_range 예외
int value = vec.at(10); // 예외 발생, 프로그램 종료 방지
// ✅ 접근 전 검사
if (10 < vec.size()) {
int value = vec[10];
}
자주 겪는 문제 시나리오들
시나리오 1: reserve 후 인덱스로 직접 접근
reserve(100)은 capacity만 늘립니다. size()는 그대로 0입니다. vec[0] = 42처럼 접근하면 미정의 동작입니다. resize() 또는 push_back()으로 원소를 채워야 합니다.
시나리오 2: erase 루프에서 반복자 무효화
vec.erase(it) 후 it는 무효화됩니다. ++it를 하면 미정의 동작입니다. erase는 삭제된 원소의 다음 원소를 가리키는 반복자를 반환하므로, it = vec.erase(it)처럼 반환값을 받아야 합니다.
시나리오 3: push_back만 반복해 100만 개 넣는데 10초
빈 vector에 push_back을 반복하면 capacity가 부족할 때마다 재할당(2배 확장)이 발생합니다. 100만 개면 약 20번 재할당 × 평균 50만 개 복사로 병목이 됩니다. reserve(예상_개수)로 미리 공간을 잡으면 재할당을 막을 수 있습니다.
시나리오 4: 범위 기반 for에서 원소 수정·삭제
for (auto& x : vec) 루프 안에서 vec.push_back()이나 vec.erase()를 호출하면 반복자가 무효화되어 미정의 동작입니다. 수정·삭제가 필요하면 일반 for문과 반복자를 사용하세요.
시나리오 5: data() 포인터를 C API에 넘긴 뒤 vector 수정
vec.data()로 얻은 포인터는 push_back/insert/erase 후 무효화됩니다. C API 호출 중에는 vector를 수정하지 마세요.
시나리오 6: push_back으로 임시 객체를 반복 생성
vec.push_back(MyClass(a, b))처럼 호출하면 매번 임시 객체가 생성된 뒤 복사 또는 이동됩니다. emplace_back(a, b)를 쓰면 컨테이너 내부에서 직접 생성해 임시 객체와 복사 비용을 없앨 수 있습니다.
시나리오 7: vector 반환 시 불필요한 복사
C++11부터는 이동 의미론으로 return vec 시 자동 이동이 적용됩니다. 큰 vector를 값으로 반환해도 O(1) 비용으로 전달됩니다.
이 글을 읽으면:
- std::vector의 초기화·연산·용량 관리를 이해할 수 있습니다.
- 자주 겪는 에러와 해결법을 알 수 있습니다.
- 성능 최적화 팁을 적용할 수 있습니다.
- 프로덕션에서 검증된 패턴을 배울 수 있습니다.
vector vs 다른 컨테이너를 요약하면 아래와 같습니다.
flowchart TB
subgraph choice["컨테이너 선택"]
A[동적 배열이 필요할 때] --> B{용도}
B -->|인덱스 접근·끝 추가| C["std vector"]
B -->|앞뒤 삽입/삭제| D["std deque"]
B -->|키로 검색| E["std map / unordered_map"]
B -->|정렬 유지| F["std set"]
end
subgraph vector_opt["vector 최적화"]
C --> G["reserve로 재할당 최소화"]
C --> H["emplace_back으로 복사 제거"]
end
목차
- 문제 시나리오: 실제로 겪는 vector 문제들
- vector 완전 초기화 가이드
- vector 연산 완전 정리
- capacity와 용량 관리
- 완전한 vector 예제 모음
- 자주 발생하는 에러와 해결법
- 성능 최적화 팁
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
1. 문제 시나리오: 실제로 겪는 vector 문제들
시나리오 1: CSV 파싱 후 빈 행에서 크래시
파일에서 한 줄씩 읽어 vector에 저장할 때, 빈 줄이 있으면 vec[0] 접근 시 크래시가 발생합니다.
#include <sstream>
#include <string>
#include <vector>
// ❌ 위험: 빈 행 처리 없음
std::vector<std::string> parseLine(const std::string& line) {
std::vector<std::string> result;
std::istringstream iss(line);
std::string cell;
while (std::getline(iss, cell, ',')) {
result.push_back(cell);
}
return result;
}
// 호출부에서
auto cells = parseLine(""); // result는 빈 vector
std::string first = cells[0]; // ❌ segmentation fault!
해결:
// ✅ 빈 vector 검사
if (!cells.empty()) {
std::string first = cells[0];
}
// 또는
std::string first = cells.empty() ? "" : cells[0];
시나리오 2: 조건에 맞는 원소만 남기려다 무한 루프
루프 안에서 erase를 할 때 반복자 처리를 잘못하면 무한 루프나 크래시가 발생합니다.
// ❌ 잘못된 예: erase 후 it 그대로 사용
std::vector<int> vec = {1, 2, 2, 3, 2, 4};
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 2) {
vec.erase(it); // it 무효화! ++it 시 미정의 동작
}
}
해결: erase-remove 관용구 또는 erase 반환값 사용.
// ✅ erase-remove (O(n))
vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());
// ✅ erase 반환값 사용
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it == 2) {
it = vec.erase(it);
} else {
++it;
}
}
시나리오 3: 대량 데이터 로드 시 메모리 재할당 병목
로그 파일이나 CSV를 파싱할 때 reserve 없이 push_back만 하면 재할당이 반복되어 느려집니다.
// ❌ 느림: 재할당 20회 이상
std::vector<Record> loadRecords(const std::string& path) {
std::vector<Record> records;
std::ifstream file(path);
Record r;
while (readRecord(file, r)) {
records.push_back(r); // capacity 부족 시마다 재할당
}
return records;
}
해결: 파일 크기나 헤더에서 개수를 읽어 reserve 호출.
// ✅ 빠름: reserve로 재할당 0회
std::vector<Record> loadRecords(const std::string& path) {
size_t estimated = estimateRecordCount(path);
std::vector<Record> records;
records.reserve(estimated);
// ...
}
시나리오 4: vector를 함수에 넘길 때 복사 비용
큰 vector를 값으로 넘기면 전체 복사가 발생합니다.
// ❌ 느림: 100만 개 복사
void process(std::vector<int> data) { /* ... */ }
// ✅ 읽기만 할 때: const 참조
void process(const std::vector<int>& data) { /* ... */ }
// ✅ 소유권 이전: 이동
void takeOwnership(std::vector<int>&& data) { /* ... */ }
2. vector 완전 초기화 가이드
기본 초기화 방법
vector를 만드는 방법은 여러 가지입니다. 용도에 맞는 초기화를 선택하면 코드가 명확해지고 불필요한 할당을 줄일 수 있습니다.
#include <vector>
#include <iostream>
int main() {
// 1. 빈 vector
std::vector<int> v1;
// 2. 크기만 지정 (기본값으로 초기화, int는 0)
std::vector<int> v2(5); // {0, 0, 0, 0, 0}
// 3. 크기 + 초기값
std::vector<int> v3(5, 42); // {42, 42, 42, 42, 42}
// 4. 초기화 리스트 (C++11)
std::vector<int> v4 = {1, 2, 3, 4, 5};
// 5. 복사 생성
std::vector<int> v5 = v4;
// 6. 이동 생성 (C++11)
std::vector<int> v6 = std::move(v4); // v4는 빈 상태
// 7. 반복자 범위로 복사
std::vector<int> v7(v5.begin(), v5.end());
// 8. 부분 범위 복사
std::vector<int> v8(v5.begin() + 1, v5.begin() + 4); // {2, 3, 4}
return 0;
}
위 코드 설명:
v2(5): 크기 5, 기본값(0)으로 채움.v2(5, 42)와 구분—괄호는 생성자, 중괄호는 리스트.v4 = {1,2,3,4,5}: 초기화 리스트로 값을 직접 나열.v6 = std::move(v4): v4의 버퍼를 v6로 이동. v4는 빈 vector가 됨.v8(begin+1, begin+4): 반복자 범위 [begin, end)로 부분 복사.
v2(5) vs v2{5} 차이
std::vector<int> a(5); // 크기 5, 값 {0,0,0,0,0}
std::vector<int> b{5}; // 원소 1개, 값 {5}
std::vector<int> c(5, 2); // 크기 5, 값 {2,2,2,2,2}
std::vector<int> d{5, 2}; // 원소 2개, 값 {5, 2}
위 코드 설명: 괄호 ()는 생성자 인자, 중괄호 {}는 초기화 리스트입니다. a(5)는 “크기 5”, b{5}는 “원소 5 하나”입니다.
사용자 정의 타입 초기화
struct Point {
int x, y;
Point(int x, int y) : x(x), y(y) {}
};
// emplace_back: 생성자 인자로 직접 생성 (임시 객체 없음)
std::vector<Point> points;
points.emplace_back(1, 2); // Point(1, 2) 직접 생성
points.push_back(Point(3, 4)); // 임시 생성 후 이동
3. vector 연산 완전 정리
원소 추가
std::vector<int> vec = {1, 2, 3};
// 끝에 추가
vec.push_back(4); // 값 복사 또는 이동
vec.emplace_back(5); // 생성자 인자로 직접 생성 (복사/이동 최소화)
// 특정 위치에 삽입 (느림: O(n), 뒤 원소들 이동)
vec.insert(vec.begin(), 0); // 맨 앞에 0
vec.insert(vec.begin() + 2, 99); // 인덱스 2에 99
vec.insert(vec.end(), {10, 11, 12}); // 끝에 여러 개
위 코드 설명: push_back은 값을 넘기고, emplace_back은 생성자 인자를 넘겨 컨테이너 내부에서 직접 생성합니다. 복사/이동 비용이 큰 타입일수록 emplace_back이 유리합니다. insert는 해당 위치 이후 원소를 뒤로 밀어야 하므로 O(n)입니다.
원소 삭제
std::vector<int> vec = {1, 2, 3, 4, 5};
vec.pop_back(); // 끝에서 제거, O(1)
vec.erase(vec.begin()); // 첫 원소 제거, O(n)
vec.erase(vec.begin(), vec.end()); // 모두 제거 (clear와 유사)
vec.clear(); // size=0, capacity 유지
위 코드 설명: pop_back은 O(1)입니다. erase(반복자)는 해당 원소를 제거하고 다음 유효한 반복자를 반환합니다. clear()는 size만 0으로 하고 capacity는 그대로 두어, 나중에 push_back 시 재할당을 피할 수 있습니다.
원소 접근
std::vector<int> vec = {10, 20, 30};
// 인덱스 접근 (범위 검사 없음, 빠름)
int a = vec[0]; // 10
int b = vec[1]; // 20
// 범위 검사 (벗어나면 std::out_of_range 예외)
int c = vec.at(2); // 30
// 첫/끝 원소
int first = vec.front(); // 10
int last = vec.back(); // 30
// C API용 연속 메모리 포인터
int* ptr = vec.data(); // &vec[0]과 동일 (빈 vector 제외)
위 코드 설명: []는 범위 검사 없이 빠르지만, 범위 밖 접근 시 미정의 동작입니다. 디버그 빌드나 검증이 필요하면 at()을 사용하세요. data()는 연속 메모리 포인터를 반환하며, vector 수정 시 무효화됩니다.
크기 및 용량
std::vector<int> vec = {1, 2, 3};
size_t n = vec.size(); // 현재 원소 개수 (3)
bool e = vec.empty(); // size == 0
size_t cap = vec.capacity(); // 재할당 없이 담을 수 있는 최대 개수
vec.reserve(100); // capacity 최소 100으로 (size 변경 없음)
vec.resize(10); // size를 10으로, 부족하면 0으로 채움
vec.resize(5); // size를 5로 줄임 (뒤 원소 제거)
vec.shrink_to_fit(); // capacity를 size에 맞게 줄이기 (요청)
위 코드 설명: reserve(n)은 capacity만 늘리고 size는 그대로입니다. resize(n)은 size를 n으로 만들고, 부족하면 기본값으로 채웁니다. shrink_to_fit()은 구현에 요청할 뿐, 반드시 줄어든다는 보장은 없습니다.
반복자
std::vector<int> vec = {1, 2, 3, 4, 5};
// 순방향
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
// 역방향
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
std::cout << *it << " ";
}
// 범위 기반 for (C++11)
for (const auto& x : vec) {
std::cout << x << " ";
}
위 코드 설명: begin()/end()는 순방향, rbegin()/rend()는 역방향 반복자입니다. 읽기만 할 때는 const auto&로 참조해 복사를 피하세요.
4. capacity와 용량 관리
size vs capacity
flowchart LR
subgraph vec["vector (size=3, capacity=4)"]
direction TB
V0[""(0"] 10"]
V1[""(1"] 20"]
V2[""(2"] 30"]
V3[""(3"] (빈 공간)"]
V0 --> V1 --> V2 --> V3
end
size["size() = 3br/유효한 원소 개수"]
cap["capacity() = 4br/재할당 없이 담을 수 있는 최대 개수"]
위 다이어그램 설명: size는 실제 원소 개수, capacity는 할당된 버퍼 크기입니다. size가 capacity에 도달하면 다음 push_back 시 재할당이 발생합니다.
reserve: 미리 공간 확보
#include <iostream>
#include <vector>
std::vector<int> vec;
vec.reserve(1000); // capacity 최소 1000, size는 0
for (int i = 0; i < 1000; ++i) {
vec.push_back(i); // 재할당 없음
}
std::cout << "size: " << vec.size() << ", capacity: " << vec.capacity() << "\n";
// size: 1000, capacity: 1000
위 코드 설명: reserve(1000)은 size를 바꾸지 않고 capacity만 늘립니다. 그 다음 1000번 push_back해도 재할당이 없어 대량 삽입이 빠릅니다.
capacity 성장 (reserve 없이)
flowchart LR
subgraph growth["capacity 2배 성장"]
G0["0"] --> G1["1"]
G1 --> G2["2"]
G2 --> G3["4"]
G3 --> G4["8"]
G4 --> G5["16"]
G5 --> G6["..."]
end
위 다이어그램 설명: reserve 없이 push_back을 반복하면 capacity가 0→1→2→4→8→16…처럼 2배씩 증가합니다. 100만 개 삽입 시 약 20회 재할당이 발생합니다.
shrink_to_fit: 불필요한 메모리 해제
std::vector<int> vec(1000);
vec.resize(10); // size=10, capacity=1000 (메모리 1000개치 유지)
vec.shrink_to_fit(); // capacity를 size에 맞게 줄이기 요청
// 대부분 구현에서 capacity ≈ 10
위 코드 설명: resize로 size를 줄여도 capacity는 그대로입니다. shrink_to_fit()은 “size에 맞게 capacity를 줄여도 된다”고 요청하며, 구현이 따라주면 불필요한 메모리를 반환합니다.
5. 완전한 vector 예제 모음
예제 1: push_back vs emplace_back 완전 비교
push_back은 값을 넘기고, emplace_back은 생성자 인자를 넘겨 컨테이너 내부에서 직접 생성합니다. 복사/이동 비용이 큰 타입일수록 emplace_back이 유리합니다.
struct Item { int id; std::string name; Item(int i, std::string n) : id(i), name(std::move(n)) {} };
std::vector<Item> items;
items.reserve(4);
// push_back: 임시 Item 생성 → 이동 (생성 2회)
items.push_back(Item(1, "A"));
// emplace_back: 내부에서 직접 생성 (생성 1회)
items.emplace_back(2, "B");
// push_back + std::move: 기존 객체 이동
Item temp(3, "C");
items.push_back(std::move(temp));
// emplace_back이 가장 효율적
items.emplace_back(4, "D");
설명: emplace_back 사용 시 불필요한 임시 객체 생성이 줄어듭니다.
예제 2: reserve와 capacity (size vs capacity)
reserve는 capacity만 늘리고 size는 0입니다. push_back으로 채운 뒤 shrink_to_fit으로 불필요한 공간을 반환할 수 있습니다.
std::vector<int> vec;
vec.reserve(100); // size=0, capacity=100
for (int i = 0; i < 50; ++i) vec.push_back(i); // 재할당 없음
vec.shrink_to_fit(); // capacity ≈ 50
예제 3: 다양한 반복(iteration) 방식
std::vector<int> vec = {10, 20, 30, 40, 50};
// 1. 인덱스 기반 (수정 시 안전)
for (size_t i = 0; i < vec.size(); ++i) { /* vec[i] */ }
// 2. 반복자 (erase 시 it = vec.erase(it) 필요)
for (auto it = vec.begin(); it != vec.end(); ++it) { /* *it */ }
// 3. 범위 기반 for (읽기 전용, 수정/삭제 시 위험)
for (const auto& x : vec) { /* x */ }
// 4. 역방향
for (auto it = vec.rbegin(); it != vec.rend(); ++it) { /* *it */ }
주의: 범위 기반 for 루프 안에서 push_back/erase를 호출하면 반복자 무효화로 미정의 동작입니다.
예제 4: vector와 이동 의미론
std::vector<int> a = {1, 2, 3, 4, 5};
// 이동 생성: a의 버퍼를 b로 이전, a는 빈 상태
std::vector<int> b = std::move(a); // a.size()=0, b.size()=5
// 함수 반환 시 자동 이동 (std::move 불필요)
auto getVec = {
std::vector<int> v = {10, 20, 30};
return v; // RVO 또는 이동
};
std::vector<int> c = getVec();
예제 5: 다양한 초기화와 기본 연산
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o vec_basic vec_basic.cpp && ./vec_basic
#include <vector>
#include <iostream>
int main() {
// 초기화
std::vector<int> vec = {10, 20, 30};
// 추가
vec.push_back(40);
vec.emplace_back(50);
// 접근
std::cout << "첫 원소: " << vec.front() << "\n";
std::cout << "끝 원소: " << vec.back() << "\n";
std::cout << "크기: " << vec.size() << "\n";
// 순회
for (size_t i = 0; i < vec.size(); ++i) {
std::cout << vec[i] << " ";
}
std::cout << "\n";
// 범위 기반 for
for (const auto& x : vec) {
std::cout << x << " ";
}
std::cout << "\n";
return 0;
}
실행 결과: 첫 원소: 10, 끝 원소: 50, 크기: 5, 그 다음 두 줄에 10 20 30 40 50 출력.
예제 6: reserve + emplace_back 최적화
#include <vector>
#include <string>
#include <fstream>
#include <sstream>
struct Record {
int id;
std::string name;
double value;
Record(int i, std::string n, double v) : id(i), name(std::move(n)), value(v) {}
};
std::vector<Record> loadRecords(const std::string& filename) {
std::vector<Record> records;
records.reserve(100000); // 재할당 방지
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
std::istringstream iss(line);
int id;
std::string name;
double value;
if (iss >> id >> name >> value) {
records.emplace_back(id, std::move(name), value);
}
}
return records; // RVO 또는 이동
}
위 코드 설명: reserve로 재할당을 막고, emplace_back으로 임시 객체 없이 직접 생성합니다. std::move(name)으로 string 복사를 피합니다.
예제 7: erase-remove로 조건부 삭제
#include <vector>
#include <algorithm>
// 값 2 제거
std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};
vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());
// vec = {1, 3, 4, 5}
// 조건부 제거: 짝수만 제거
vec = {1, 2, 3, 4, 5, 6};
vec.erase(std::remove_if(vec.begin(), vec.end(),
{ return x % 2 == 0; }), vec.end());
// vec = {1, 3, 5}
위 코드 설명: std::remove는 “지울 값이 아닌 것”을 앞으로 모은 뒤 새 논리적 끝 반복자를 반환합니다. erase(그 반복자, end())로 한 번에 제거하면 O(n)입니다.
예제 8: 두 vector 합치기
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {4, 5, 6};
// 방법 1: insert
a.insert(a.end(), b.begin(), b.end());
// 방법 2: reserve + insert (더 빠름)
std::vector<int> c;
c.reserve(a.size() + b.size());
c.insert(c.end(), a.begin(), a.end());
c.insert(c.end(), b.begin(), b.end());
예제 9: 중복 제거 (정렬 후 unique)
std::vector<int> vec = {3, 1, 2, 2, 1, 3};
std::sort(vec.begin(), vec.end());
vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
// vec = {1, 2, 3}
6. 자주 발생하는 에러와 해결법
문제 1: “vector subscript out of range” 또는 segmentation fault
원인: []로 접근할 때 인덱스가 size() 범위를 벗어남.
해결법:
// ❌ 나쁜 예
std::vector<int> vec = {1, 2, 3};
int value = vec[10]; // 미정의 동작
// ✅ at() 사용 (예외 발생)
int value = vec.at(10); // std::out_of_range
// ✅ 접근 전 검사
if (index < vec.size()) {
int value = vec[index];
}
문제 2: erase 루프에서 반복자 무효화
원인: vec.erase(it) 후 it는 무효화됨. ++it 시 미정의 동작.
해결법:
// ❌ 나쁜 예
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 2) {
vec.erase(it); // it 무효화!
}
}
// ✅ erase 반환값 사용
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it == 2) {
it = vec.erase(it);
} else {
++it;
}
}
문제 3: reserve 후 size는 그대로
원인: reserve(n)은 capacity만 늘림. size는 0.
해결법:
// ❌ 나쁜 예
std::vector<int> vec;
vec.reserve(100);
vec[0] = 42; // size=0인데 [0] 접근! 미정의 동작
// ✅ push_back 또는 resize
vec.reserve(100);
for (int i = 0; i < 100; ++i) {
vec.push_back(i);
}
// 또는
vec.resize(100);
vec[0] = 42; // OK
문제 4: push_back/insert 중 반복자 무효화
원인: vector 수정 시 기존 반복자 무효화.
해결법: 루프 안에서 수정할 때는 인덱스 사용.
// ❌ 위험: 반복자 사용 중 push_back
for (auto it = vec.begin(); it != vec.end(); ++it) {
vec.push_back(*it); // 반복자 무효화!
}
// ✅ 인덱스 사용
for (size_t i = 0; i < vec.size(); ++i) {
vec.push_back(vec[i]); // 주의: 무한 루프 가능, 예시용
}
문제 5: data() 포인터 무효화
원인: vec.data()로 얻은 포인터는 push_back/insert/erase 후 무효화.
해결법: C API 호출 중에는 vector를 수정하지 마세요.
int* ptr = vec.data();
process_c_api(ptr, vec.size()); // OK
vec.push_back(1); // ptr 무효화!
process_c_api(ptr, vec.size()); // ❌ 미정의 동작
문제 6: vector<bool> 특수화
원인: std::vector<bool>은 비트로 압축되어 operator[]가 bool&가 아닌 프록시를 반환.
해결법: bool 시퀀스가 필요하면 std::vector<char> 또는 std::vector<uint8_t> 사용.
// ❌ vector<bool>은 특수화됨
std::vector<bool> flags = {true, false};
// bool& ref = flags[0]; // 컴파일 에러
// ✅ 대안
std::vector<uint8_t> flags = {1, 0};
문제 7: signed/unsigned 비교
원인: vec.size()는 size_t(unsigned). int i와 비교 시 경고.
해결법:
// ✅ size_t 사용
for (size_t i = 0; i < vec.size(); ++i) { }
// ✅ 범위 기반 for
for (const auto& x : vec) { }
문제 8: 빈 vector에 front/back 접근
원인: vec.front()와 vec.back()은 빈 vector에서 미정의 동작.
해결법:
if (!vec.empty()) {
int first = vec.front();
int last = vec.back();
}
문제 9: 초기화 리스트 vs 생성자 혼동
원인: std::vector<int> v(5)와 std::vector<int> v{5}는 다릅니다. 괄호는 생성자 인자, 중괄호는 초기화 리스트.
해결법:
std::vector<int> a(5); // 크기 5, {0,0,0,0,0}
std::vector<int> b{5}; // 원소 1개, {5}
std::vector<int> c(5, 2); // 크기 5, 값 2, {2,2,2,2,2}
std::vector<int> d{5, 2}; // 원소 2개, {5, 2}
문제 10: 범위 기반 for에서 원소 삭제
원인: for (auto& x : vec) 루프 안에서 vec.erase()나 vec.push_back()을 호출하면 반복자 무효화.
해결법:
// ❌ 위험
for (auto& x : vec) {
if (x == 2) vec.erase(/* ... */); // 미정의 동작
}
// ✅ erase-remove 또는 반복자 루프
vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());
문제 11: reserve 후 resize 없이 인덱스 접근
원인: reserve는 capacity만 늘립니다. vec[0]은 size가 1 이상일 때만 유효합니다.
해결법:
vec.reserve(100);
vec.resize(100); // 또는 push_back/emplace_back으로 채우기
vec[0] = 42; // OK
7. 성능 최적화 팁
팁 1: reserve로 재할당 최소화
// ❌ 느림: 재할당 20회
std::vector<int> vec;
for (int i = 0; i < 1000000; ++i) {
vec.push_back(i);
}
// ✅ 빠름: 재할당 0회
std::vector<int> vec;
vec.reserve(1000000);
for (int i = 0; i < 1000000; ++i) {
vec.push_back(i);
}
팁 2: emplace_back vs push_back
struct Point { int x, y; Point(int x, int y) : x(x), y(y) {} };
// push_back: 임시 객체 생성 후 복사/이동
vec.push_back(Point(1, 2));
// emplace_back: 직접 생성 (임시 없음)
vec.emplace_back(1, 2);
팁 3: 범위 기반 for에서 참조 사용
std::vector<std::string> vec = {"a", "b", "c"};
// ❌ 복사 발생
for (std::string s : vec) { }
// ✅ 참조로 복사 방지
for (const std::string& s : vec) { }
// 또는
for (const auto& s : vec) { }
팁 4: erase-remove idiom
// ❌ 루프에서 erase: O(n²)에 가까움
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it == 2) it = vec.erase(it);
else ++it;
}
// ✅ erase-remove: O(n)
vec.erase(std::remove(vec.begin(), vec.end(), 2), vec.end());
팁 5: clear() 후 재사용 (capacity 유지)
std::vector<int> vec;
vec.reserve(1000);
// ... 사용 ...
vec.clear(); // size=0, capacity 유지
// 다시 push_back 시 재할당 없음
성능 비교 표
| 시나리오 | reserve 없음 | reserve 사용 | 개선 |
|---|---|---|---|
| 100만 개 int push_back | ~50ms | ~10ms | 5배 |
| 10만 개 string push_back | ~120ms | ~25ms | 4.8배 |
| emplace_back vs push_back (복사 비용 큰 타입) | 2배 | 1배 | 2배 |
8. 베스트 프랙티스
1. 예상 크기를 알면 반드시 reserve
// ✅ 대량 삽입 전 reserve
std::vector<Record> records;
records.reserve(estimated_count);
for (/* ... */) {
records.emplace_back(/* ... */);
}
2. 복사 비용이 큰 타입은 emplace_back 또는 std::move
// ✅ string, 사용자 정의 타입
vec.emplace_back(1, "name", 3.14);
vec.push_back(std::move(existing_object));
3. 읽기 전용 순회 시 const 참조 사용
// ✅ 복사 방지
for (const auto& item : vec) {
process(item);
}
4. 삭제는 erase-remove idiom
// ✅ O(n) 한 번에 삭제
vec.erase(std::remove_if(vec.begin(), vec.end(), predicate), vec.end());
5. 인덱스 검증이 필요하면 at() 사용
// ✅ 디버그/검증 시
int value = vec.at(index); // 범위 밖이면 std::out_of_range
6. 함수 인자: 읽기만 하면 const 참조
// ✅ 복사 없음
void process(const std::vector<int>& data);
// ✅ 소유권 이전
void take(std::vector<int>&& data);
7. vector 반환 시 값으로 반환 (이동/RVO)
// ✅ C++11 이후: 복사 없이 이동
std::vector<int> loadData() {
std::vector<int> result;
// ...
return result;
}
8. data() 사용 중 vector 수정 금지
int* p = vec.data() 후 vec.push_back() 등 수정 시 p가 무효화됩니다. C API 호출이 끝날 때까지 vector를 수정하지 마세요.
9. 프로덕션 패턴
패턴 1: 버퍼 풀 (재사용)
class RequestHandler {
std::vector<char> read_buffer_;
public:
void handle(const char* data, size_t len) {
read_buffer_.clear();
if (read_buffer_.capacity() < len) {
read_buffer_.reserve(len);
}
read_buffer_.assign(data, data + len);
}
};
위 코드 설명: clear()는 size만 0으로 하고 capacity는 유지합니다. 같은 버퍼를 재사용해 재할당을 줄입니다.
패턴 2: 예상 크기 추정
std::vector<Record> loadFromFile(const std::string& path) {
auto size = std::filesystem::file_size(path);
size_t estimated = std::max(size / sizeof(Record), size_t(1000));
std::vector<Record> records;
records.reserve(estimated);
// ... 로드
return records;
}
패턴 3: vector 반환 (RVO/이동)
// ✅ 값 반환: C++11에서 이동 또는 RVO
std::vector<int> getData() {
std::vector<int> result;
result.reserve(1000);
// ... 채우기
return result; // 복사 없음
}
패턴 4: 조건부 reserve
void addItems(std::vector<int>& vec, const std::vector<int>& newItems) {
if (vec.capacity() - vec.size() < newItems.size()) {
vec.reserve(vec.size() + newItems.size());
}
for (int x : newItems) {
vec.push_back(x);
}
}
패턴 5: CSV/로그 파싱 (reserve + emplace_back)
std::vector<std::vector<std::string>> parseCSV(std::istream& in) {
std::vector<std::vector<std::string>> rows;
std::string line;
while (std::getline(in, line)) {
std::vector<std::string> row;
row.reserve(16);
std::istringstream iss(line);
std::string cell;
while (std::getline(iss, cell, ',')) {
row.emplace_back(std::move(cell));
}
rows.push_back(std::move(row));
}
return rows;
}
프로덕션 체크리스트
- 대량 삽입 전
reserve(예상_개수)호출 - 반복 사용하는 vector는
clear()후 재사용 (capacity 유지) - 복사 비용 큰 타입은
emplace_back또는push_back(std::move(x)) -
data()포인터 사용 중 vector 수정 금지 - 프로파일러로 실제 병목 확인 후 최적화
10. 구현 체크리스트
vector 사용 시 다음을 확인하세요:
- 대량 삽입 전
reserve(예상_개수)호출 - 복사 비용이 큰 타입은
emplace_back사용 - 범위 기반 for에서 읽기만 할 때
const auto&사용 - erase 루프에서
it = vec.erase(it)반환값 반영 -
[]대신 범위 검사가 필요하면at()사용 -
vector<bool>대신vector<char>또는vector<uint8_t>고려 - 빈 vector에
front()/back()접근 전empty()검사
정리
| 항목 | 설명 |
|---|---|
| 초기화 | {}, (n), (n, val), 반복자 범위 |
| 추가 | push_back, emplace_back, insert |
| 삭제 | pop_back, erase, clear |
| 접근 | [], at(), front(), back(), data() |
| 용량 | size, capacity, reserve, resize, shrink_to_fit |
핵심 원칙:
- 크기를 알면
reserve()사용 emplace_back()선호- 범위 기반 for에서 참조 사용
erase-removeidiom 활용
자주 묻는 질문 (FAQ)
Q. reserve를 너무 크게 잡으면 어떻게 되나요?
A. 메모리만 더 사용합니다. 100만 개 넣을 곳에 1000만으로 reserve해도 동작에는 문제 없지만, 사용하지 않는 900만 개치 메모리가 남습니다.
Q. emplace_back을 항상 써야 하나요?
A. 복사/이동 비용이 있는 타입(string, 사용자 정의 클래스)일 때 유리합니다. int처럼 단순 타입은 push_back과 차이가 거의 없습니다.
Q. vector와 array의 차이는?
A. std::array<T, N>은 크기가 컴파일 타임에 고정됩니다. std::vector는 런타임에 크기가 변합니다. 크기를 모르거나 동적으로 늘어나야 하면 vector를 사용하세요.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요.
한 줄 요약: vector는 reserve로 재할당을 줄이고, capacity와 size 차이를 알면 성능을 잡을 수 있습니다. 다음으로 vector·string 성능 최적화(#10-1)를 읽어보면 좋습니다.
다음 글: C++ vector·string 성능 최적화 | reserve·emplace_back
참고 자료
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ vector 성능 | “100만 개 넣는데 10초” 문제와 reserve
- C++ STL 알고리즘 | sort·find·transform 람다와 함께 쓰기 (실전 패턴)
- C++ 범위 기반 for문과 구조화된 바인딩 | 모던 C++ 반복문
이 글에서 다루는 키워드 (관련 검색어)
C++, std::vector, vector기초, STL, 동적배열, reserve, capacity, push_back, emplace_back, 반복자 등으로 검색하시면 이 글이 도움이 됩니다.
관련 글
- C++ map·set 완벽 가이드 | ordered vs unordered· 커스텀 키
- C++ 컨테이너 선택 가이드 | vector/list/deque/map/set 상황별 선택과 성능 최적화
- C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
- C++ std::function | 콜백·전략 패턴과 함수 객체
- C++ vector 성능 |