C++ vector 기초 완벽 가이드 | 초기화·연산·용량 관리와 실전 패턴

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

목차

  1. 문제 시나리오: 실제로 겪는 vector 문제들
  2. vector 완전 초기화 가이드
  3. vector 연산 완전 정리
  4. capacity와 용량 관리
  5. 완전한 vector 예제 모음
  6. 자주 발생하는 에러와 해결법
  7. 성능 최적화 팁
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 구현 체크리스트

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~10ms5배
10만 개 string push_back~120ms~25ms4.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

핵심 원칙:

  1. 크기를 알면 reserve() 사용
  2. emplace_back() 선호
  3. 범위 기반 for에서 참조 사용
  4. erase-remove idiom 활용

자주 묻는 질문 (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 성능 |