본문으로 건너뛰기
Previous
Next
C++ vector 기초 완벽 가이드 | 초기화·연산·용량 관리와 실전 패턴

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

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

이 글의 핵심

C++ vector 기초 : 초기화·연산·용량 관리와 실전 패턴. vector 인덱스 접근에서 segmentation fault·실무에서 겪은 문제.

💡 초보자를 위한 한 줄: vector연속 메모리 + 끝쪽 push_back/emplace_back이 강점입니다. 개수를 대략 알면 reserve 한 줄이 체감이 큽니다. erase 후 반복자는 반환값으로 갱신하는 습관을 들이세요. 10-1 vector 성능과 세트로 보면 좋습니다.

🎯 이 글을 읽으면 (읽는 시간: 35분)

TL;DR: C++ STL의 핵심, vector를 완벽하게 마스터합니다. 초기화부터 용량 관리, segmentation fault 방지까지, 실무에서 바로 쓸 수 있는 모든 것을 배웁니다. 이 글을 읽으면:

  • ✅ vector 초기화, 접근, 삽입/삭제 완벽 이해
  • ✅ size() vs capacity(), reserve() vs resize() 차이 마스터
  • ✅ segmentation fault 방지 및 성능 최적화 능력 습득 실무 활용:
  • 🔥 동적 배열 관리 (자동 크기 조절)
  • 🔥 성능 최적화 (reserve로 재할당 방지)
  • 🔥 안전한 인덱스 접근 (at() vs []) 난이도: 초급 | 실습 코드: 20개 | C++ STL 필수

들어가며: 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

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

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 활용

초보자를 위한 체크리스트

  • 대량 삽입 전 reserve를 호출했는가?
  • erase 루프에서 it = vec.erase(it) 패턴을 썼는가?
  • empty()일 때 front()/back()을 호출하지 않았는가?

💡 초보자 팁: 본문 10. 구현 체크리스트·6. 자주 발생하는 에러를 함께 보면 빠짐없이 점검하기 좋습니다.


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

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ vector 기초 완벽 가이드 | 초기화·연산·용량 관리와 실전 패턴」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ vector 기초 완벽 가이드 | 초기화·연산·용량 관리와 실전 패턴」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.