C++ std::vector 기초: 생성·삽입·삭제와 배열 대신 쓰는 이유
이 글의 핵심
C 스타일 배열은 런타임에 크기를 늘릴 수 없고 범위를 벗어난 접근도 조용히 메모리를 망가뜨립니다. 이 글은 vector 입문자를 위해 기본 사용법과 학생 점수 관리·게임 맵 예제를 제공하고, 반복자 무효화와 불필요한 복사처럼 초보 단계에서 가장 자주 부딪히는 문제의 원인과 고치는 법을 함께 설명합니다.
vector가 배열보다 좋은 이유
C 스타일 배열은 선언할 때 크기가 고정되며, 이후 런타임에 원소를 몇 개 더 담아야 할지 알게 되어도 크기를 늘릴 방법이 없습니다. 게다가 arr[100]처럼 배열 범위를 벗어난 인덱스에 접근해도 컴파일러나 런타임이 이를 감지하지 못해 조용히 메모리를 오염시키는 버그로 이어지기 쉽습니다. std::vector는 이런 문제를 근본적으로 해결합니다. 내부적으로 힙에 할당된 동적 배열을 관리하며, 용량이 부족해지면 자동으로 더 큰 메모리를 할당받아 기존 원소를 옮기고, at() 메서드를 쓰면 범위를 벗어난 접근 시 예외를 던져 문제를 즉시 드러내 줍니다.
// 배열의 문제점
int arr[100]; // 크기 고정, 변경 불가
// arr[100] = 1; // 범위 초과 체크 안 됨
// vector의 장점
vector<int> v; // 크기 자동 조절
v.push_back(1); // 동적으로 추가
// v.at(100); // 범위 체크 (예외 발생)
기본 사용법
선언과 초기화
vector는 상황에 맞는 여러 생성자를 제공합니다. 크기를 지정하지 않으면 빈 벡터로 시작하고, 정수 하나만 넘기면 그 개수만큼 기본값(정수는 0)으로 채워진 벡터가 만들어집니다. 두 번째 인자로 값을 함께 지정하면 그 값으로 초기화되고, 중괄호 초기화 리스트를 쓰면 배열 리터럴처럼 원하는 값들을 바로 나열할 수 있습니다. 벡터를 다른 벡터에 대입하면 얕은 복사가 아니라 원소 전체를 복사하는 깊은 복사가 일어난다는 점도 배열과 다른 중요한 차이입니다.
#include <vector>
using namespace std;
// 빈 벡터
vector<int> v1;
// 크기 지정
vector<int> v2(10); // 0으로 초기화된 10개
// 값과 함께 초기화
vector<int> v3(10, 5); // 5로 초기화된 10개
// 초기화 리스트
vector<int> v4 = {1, 2, 3, 4, 5};
// 다른 벡터 복사
vector<int> v5 = v4;
요소 추가/삭제
push_back으로 끝에 원소를 추가하는 것은 벡터에서 가장 저렴한 연산입니다(용량이 충분하면 O(1) 상환 시간). 반대로 insert나 erase로 벡터 중간에 원소를 넣거나 빼면, 그 뒤에 있는 모든 원소를 한 칸씩 밀거나 당겨야 하므로 O(n) 시간이 걸립니다. 자주 중간 삽입/삭제가 필요한 상황이라면 std::list나 std::deque 같은 다른 컨테이너를 고려하는 것이 좋습니다. clear()는 모든 원소를 제거하지만 할당된 용량(capacity)은 그대로 유지한다는 점도 기억해 둘 필요가 있습니다.
다만 “중간 삽입이 많으면 list”라는 조언은 교과서적인 이야기이고, 실제로 측정해 보면 수천 개 정도의 원소에서는 여전히 vector가 빠른 경우가 많습니다. vector의 원소 이동은 연속된 메모리를 한 번에 옮기는 작업이라 CPU 캐시와 잘 맞지만, list는 삽입할 위치를 찾으려고 노드 포인터를 하나씩 따라가는 동안 캐시 미스가 계속 발생하기 때문입니다. 그래서 컨테이너를 바꾸기 전에 먼저 실제 데이터 크기로 측정해 보는 것이 좋고, 순서가 중요하지 않다면 지울 원소를 마지막 원소와 바꾼 뒤 pop_back()하는 방식으로 O(1) 삭제를 만들 수도 있습니다.
vector<int> v;
// 끝에 추가
v.push_back(10);
v.push_back(20);
v.push_back(30);
// v = [10, 20, 30]
// 끝 제거
v.pop_back();
// v = [10, 20]
// 특정 위치에 삽입
v.insert(v.begin() + 1, 15);
// v = [10, 15, 20]
// 특정 위치 삭제
v.erase(v.begin() + 1);
// v = [10, 20]
// 전체 삭제
v.clear();
// v = []
접근
원소 접근에는 크게 두 가지 방법이 있습니다. operator[]는 배열처럼 빠르지만 범위를 벗어나도 검사하지 않아 정의되지 않은 동작을 일으킬 수 있고, at()은 내부적으로 범위 검사를 수행해 벗어나면 std::out_of_range 예외를 던집니다. 성능이 극도로 중요한 반복문 안에서는 operator[]를, 사용자 입력 등 외부에서 들어온 인덱스를 다룰 때는 at()을 쓰는 것이 일반적인 원칙입니다. front()와 back()은 각각 첫 원소와 마지막 원소에 안전하게 접근하는 편의 함수입니다.
vector<int> v = {10, 20, 30, 40, 50};
// 인덱스 접근
cout << v[0]; // 10 (범위 체크 안함)
cout << v.at(0); // 10 (범위 체크)
// 첫/마지막 요소
cout << v.front(); // 10
cout << v.back(); // 50
// 크기
cout << v.size(); // 5
// 비어있는지 확인
if (v.empty()) {
cout << "비어있음" << endl;
}
반복문
벡터를 순회하는 방법은 목적에 따라 세 가지로 나뉩니다. 인덱스가 실제로 필요한 경우(예: 다른 배열과 같은 위치를 참조해야 할 때)를 제외하면, 범위 기반 for가 가장 읽기 쉽고 실수할 여지가 적어 권장됩니다.
인덱스 기반
인덱스 기반 반복은 현재 위치의 인덱스 값 자체가 로직에 필요할 때(예: 다른 벡터의 같은 위치 원소와 비교) 유용합니다. 다만 v.size()가 size_t(부호 없는 정수)를 반환하는데 반복 변수를 int로 선언하면, 뒤에서 다룰 것처럼 미묘한 부호 관련 버그가 발생할 수 있어 주의가 필요합니다.
vector<int> v = {1, 2, 3, 4, 5};
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
범위 기반 for (권장)
C++11부터 도입된 범위 기반 for는 인덱스나 반복자를 직접 다루지 않고도 컨테이너의 모든 원소를 순회할 수 있게 해줍니다. for (int x : v)처럼 값으로 받으면 각 원소를 읽기만 하고(내부적으로는 복사가 일어나므로 큰 객체라면 const auto&가 낫습니다), for (int& x : v)처럼 참조로 받으면 반복 도중 원본 벡터의 원소를 직접 수정할 수 있습니다.
// 읽기 전용
for (int x : v) {
cout << x << " ";
}
// 수정 가능
for (int& x : v) {
x *= 2; // 각 요소를 2배로
}
반복자 (Iterator)
반복자는 범위 기반 for보다 한 단계 더 저수준의 도구로, it가 가리키는 위치를 직접 다루기 때문에 특정 위치에 삽입·삭제하거나 여러 반복자를 비교하는 등 범위 기반 for로는 표현할 수 없는 세밀한 제어가 필요할 때 사용합니다. STL 알고리즘 함수 대부분이 반복자 쌍(begin(), end())을 인자로 받도록 설계되어 있어, 반복자 개념을 이해해두면 sort, find 같은 알고리즘을 직접 다룰 때도 자연스럽게 응용할 수 있습니다.
for (auto it = v.begin(); it != v.end(); it++) {
cout << *it << " ";
}
2차원 벡터
행렬, 격자, 게임 맵처럼 2차원 데이터를 다뤄야 할 때는 벡터 안에 벡터를 담는 vector<vector<T>> 형태를 씁니다. 이는 C 스타일의 int matrix[3][4]와 달리 각 행의 크기를 서로 다르게 만들 수도 있고, 런타임에 행이나 열의 개수를 동적으로 정할 수 있다는 장점이 있습니다. 다만 내부적으로는 각 행이 별도의 힙 메모리 블록으로 분산되어 있어, 진짜 2차원 배열처럼 메모리가 연속적이지 않다는 점은 캐시 성능 측면에서 고려할 부분입니다. 성능이 중요한 대규모 행렬 연산이라면 1차원 벡터에 인덱스 계산(row * cols + col)으로 2차원을 흉내 내는 방식이 더 효율적일 수 있습니다.
// 2차원 벡터 선언
vector<vector<int>> matrix;
// 3x4 행렬 (0으로 초기화)
vector<vector<int>> matrix2(3, vector<int>(4, 0));
// 값 접근
matrix2[0][0] = 1;
matrix2[1][2] = 5;
// 행 추가
matrix.push_back({1, 2, 3});
matrix.push_back({4, 5, 6});
// 출력
for (int i = 0; i < matrix.size(); i++) {
for (int j = 0; j < matrix[i].size(); j++) {
cout << matrix[i][j] << " ";
}
cout << endl;
}
STL 알고리즘
<algorithm> 헤더는 벡터를 포함한 모든 STL 컨테이너에 공통으로 적용할 수 있는 정렬, 검색, 변환 등의 함수를 제공합니다. 이 함수들은 대부분 반복자 범위(begin(), end())를 인자로 받기 때문에, 벡터뿐 아니라 배열이나 다른 컨테이너에도 동일한 코드로 적용할 수 있다는 것이 큰 장점입니다.
정렬
std::sort는 내부적으로 인트로소트(introsort, 퀵소트+힙소트+삽입정렬을 조합한 알고리즘)를 사용해 평균 O(n log n) 시간에 정렬을 수행합니다. 기본은 오름차순이지만, std::greater<int>()처럼 비교 함수를 세 번째 인자로 넘기면 내림차순이나 사용자 정의 기준으로도 정렬할 수 있습니다. 정렬 결과의 안정성(같은 값의 상대 순서 유지)이 필요하다면 std::sort 대신 std::stable_sort를 사용해야 합니다.
#include <algorithm>
vector<int> v = {3, 1, 4, 1, 5, 9};
// 오름차순
sort(v.begin(), v.end());
// v = [1, 1, 3, 4, 5, 9]
// 내림차순
sort(v.begin(), v.end(), greater<int>());
// v = [9, 5, 4, 3, 1, 1]
검색
std::find는 처음부터 순서대로 값을 비교하는 선형 탐색으로, 정렬 여부와 상관없이 사용할 수 있지만 O(n) 시간이 걸립니다. 반면 std::binary_search는 이진 탐색을 수행해 O(log n)으로 훨씬 빠르지만, 사전에 벡터가 정렬되어 있어야만 올바르게 동작합니다. 정렬되지 않은 벡터에 binary_search를 쓰면 실제로 값이 존재해도 잘못된 결과(false)를 반환할 수 있으므로, 검색 전에 데이터가 정렬되어 있는지 반드시 확인해야 합니다.
vector<int> v = {1, 2, 3, 4, 5};
// 값 찾기
auto it = find(v.begin(), v.end(), 3);
if (it != v.end()) {
cout << "찾음: " << *it << endl;
}
// 이진 탐색 (정렬된 벡터)
bool found = binary_search(v.begin(), v.end(), 3);
기타
<algorithm>과 <numeric>에는 이 밖에도 자주 쓰는 유틸리티가 많습니다. reverse는 범위 안의 원소 순서를 그 자리에서 뒤집고, max_element/min_element는 값이 아니라 반복자를 반환하므로 실제 값을 얻으려면 역참조(*)해야 한다는 점에 주의해야 합니다. accumulate는 <numeric> 헤더에 있는 함수로, 세 번째 인자로 넘긴 초깃값부터 시작해 범위 안의 모든 원소를 순서대로 더한(또는 커스텀 이항 연산을 적용한) 결과를 반환합니다.
vector<int> v = {1, 2, 3, 4, 5};
// 역순
reverse(v.begin(), v.end());
// v = [5, 4, 3, 2, 1]
// 최댓값/최솟값
int maxVal = *max_element(v.begin(), v.end());
int minVal = *min_element(v.begin(), v.end());
// 합계
int sum = accumulate(v.begin(), v.end(), 0);
자주 하는 실수
실수 1: size()를 int로 받기
vector::size()는 size_t, 즉 부호 없는 정수 타입을 반환합니다. 부호 없는 값에서 뺄셈을 하면, 벡터가 비어 있을 때(size() == 0) v.size() - 1이 -1이 아니라 매우 큰 양수(64비트에서 18446744073709551615)가 됩니다. 반복문에서 i >= 0 조건으로 역순 순회를 하려는 경우 이 함정에 특히 자주 걸립니다. 부호 없는 i는 절대 0보다 작아지지 않으므로 i >= 0은 항상 참이 되어, 0에서 한 번 더 줄어드는 순간 거대한 인덱스로 벡터를 읽다가 크래시가 납니다. 결과를 먼저 int로 바꾼 뒤 뺄셈하거나, 역방향 반복자를 쓰면 됩니다.
// ❌ 위험한 코드: i는 unsigned라 i >= 0이 항상 참 → 무한 루프·범위 초과
for (size_t i = v.size() - 1; i >= 0; i--) {
// ...
}
// ❌ 빈 벡터에서 v.size() - 1은 거대한 양수
if (idx < v.size() - 1) { /* ... */ }
// ✅ 올바른 코드: int로 바꾼 뒤 뺄셈
for (int i = (int)v.size() - 1; i >= 0; i--) {
// ...
}
// ✅ 또는 역방향 반복자
for (auto it = v.rbegin(); it != v.rend(); ++it) {
// ...
}
GCC·Clang에서 -Wall -Wextra를 켜면 comparison of unsigned expression in '>= 0' is always true 경고로 첫 번째 실수를 잡아 주고, int i < v.size() 비교에는 -Wsign-compare 경고가 뜹니다. C++20에서는 부호 있는 크기를 돌려주는 std::ssize(v)를 쓰면 이런 캐스팅을 줄일 수 있습니다.
실수 2: 반복 중 삭제
정방향으로 순회하며 erase로 원소를 지우면, 삭제된 원소 뒤에 있던 모든 원소가 한 칸씩 앞으로 당겨지면서 인덱스가 밀립니다. 이 상태에서 i를 그대로 증가시키면 방금 당겨진 원소를 건너뛰고 지나가게 되어, 연속된 target 값이 있을 때 일부를 놓치는 버그가 발생합니다. 뒤에서부터 앞으로 순회하며 삭제하면 이미 처리한 뒤쪽 인덱스는 앞쪽 삭제에 영향받지 않으므로 이 문제를 피할 수 있습니다.
// ❌ 잘못된 코드
for (int i = 0; i < v.size(); i++) {
if (v[i] == target) {
v.erase(v.begin() + i); // 인덱스 꼬임!
}
}
// ✅ 올바른 코드
for (int i = (int)v.size() - 1; i >= 0; i--) {
if (v[i] == target) {
v.erase(v.begin() + i);
}
}
// ✅ 더 나은 코드: erase-remove 관용구 (C++20이면 std::erase)
v.erase(remove(v.begin(), v.end(), target), v.end());
// C++20: std::erase(v, target);
역순 삭제는 동작은 맞지만, erase를 부를 때마다 뒤쪽 원소를 당기므로 지울 원소가 많으면 O(n²)이 됩니다. remove는 남길 원소를 앞으로 모아 두고 새 끝 위치를 돌려줄 뿐 실제로 크기를 줄이지는 않기 때문에, 반드시 erase와 짝지어 호출해야 합니다. remove만 호출하고 끝내면 크기가 그대로라서 “지웠는데 원소가 남아 있다”는 증상이 나타나는데, 처음 이 관용구를 쓸 때 거의 누구나 한 번씩 겪는 실수입니다. 조건으로 지우려면 remove_if에 람다를 넘기면 됩니다.
실수 3: 범위 초과
operator[]는 성능을 위해 범위를 검사하지 않도록 설계되었습니다. 크기가 10인 벡터에서 v[10]처럼 유효 범위를 벗어난 인덱스에 접근해도 컴파일은 물론 즉시 크래시가 나지 않을 수도 있어(정의되지 않은 동작이므로 결과를 예측할 수 없음), 버그가 한참 뒤에 다른 곳에서 이상한 증상으로 나타나는 경우가 많습니다. 인덱스 값이 외부 입력이나 계산 결과처럼 확신할 수 없는 경우에는 at()을 사용해 문제를 즉시 예외로 드러내는 편이 디버깅 시간을 크게 줄여줍니다.
vector<int> v(10);
// ❌ 범위 초과
v[10] = 1; // 정의되지 않은 동작 (크래시할 수도, 조용히 옆 메모리를 덮어쓸 수도 있음)
// ✅ 안전한 방법
v.at(10) = 1; // std::out_of_range 예외 발생
개발 중에는 표준 라이브러리의 디버그 모드를 켜 두면 operator[]의 범위 초과도 잡을 수 있습니다. GCC의 libstdc++는 -D_GLIBCXX_ASSERTIONS, MSVC는 Debug 빌드에서 기본으로 범위 검사가 들어가 “vector subscript out of range” 대화상자를 띄웁니다. AddressSanitizer(-fsanitize=address)도 힙 버퍼 오버플로를 정확한 줄 번호와 함께 알려 주므로, 크래시 위치가 매번 바뀌는 이상한 버그를 만나면 먼저 켜 볼 만합니다.
실전 예시
아래 세 가지 예시는 통계 집계, 데이터 필터링, 2차원 격자 관리라는 실무에서 벡터가 실제로 어떻게 활용되는지 보여줍니다. 각각 지금까지 다룬 요소 추가/삭제, STL 알고리즘, 2차원 벡터를 하나의 완결된 프로그램으로 조합한 것입니다.
예시 1: 학생 점수 관리 시스템
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
class ScoreManager {
private:
vector<int> scores;
public:
// 점수 추가
void addScore(int score) {
if (score >= 0 && score <= 100) {
scores.push_back(score);
cout << score << "점 추가됨" << endl;
} else {
cout << "잘못된 점수입니다 (0-100)" << endl;
}
}
// 평균 계산
double getAverage() const {
if (scores.empty()) return 0.0;
return (double)accumulate(scores.begin(), scores.end(), 0) / scores.size();
}
// 최고/최저 점수
void printMinMax() const {
if (scores.empty()) {
cout << "점수가 없습니다" << endl;
return;
}
cout << "최고 점수: " << *max_element(scores.begin(), scores.end()) << endl;
cout << "최저 점수: " << *min_element(scores.begin(), scores.end()) << endl;
}
// 점수 분포
void printDistribution() const {
vector<int> dist(5, 0); // A, B, C, D, F
for (int score : scores) {
if (score >= 90) dist[0]++;
else if (score >= 80) dist[1]++;
else if (score >= 70) dist[2]++;
else if (score >= 60) dist[3]++;
else dist[4]++;
}
cout << "=== 점수 분포 ===" << endl;
cout << "A (90-100): " << dist[0] << "명" << endl;
cout << "B (80-89): " << dist[1] << "명" << endl;
cout << "C (70-79): " << dist[2] << "명" << endl;
cout << "D (60-69): " << dist[3] << "명" << endl;
cout << "F (0-59): " << dist[4] << "명" << endl;
}
// 정렬된 점수 출력
void printSorted() const {
vector<int> sorted = scores; // 복사
sort(sorted.begin(), sorted.end(), greater<int>()); // 내림차순
cout << "=== 점수 순위 ===" << endl;
for (int i = 0; i < sorted.size(); i++) {
cout << (i + 1) << "등: " << sorted[i] << "점" << endl;
}
}
};
int main() {
ScoreManager sm;
// 점수 추가
sm.addScore(85);
sm.addScore(92);
sm.addScore(78);
sm.addScore(95);
sm.addScore(88);
// 통계 출력
cout << "\n평균: " << sm.getAverage() << "점" << endl;
sm.printMinMax();
cout << endl;
sm.printDistribution();
cout << endl;
sm.printSorted();
return 0;
}
설명: vector를 활용한 학생 점수 관리 시스템입니다. 점수 추가, 평균 계산, 최고/최저 점수, 점수 분포, 순위 등 실무에서 자주 사용하는 기능들을 구현했습니다.
예시 2: 동적 배열을 활용한 필터링
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 짝수만 필터링
vector<int> filterEven(const vector<int>& numbers) {
vector<int> result;
for (int num : numbers) {
if (num % 2 == 0) {
result.push_back(num);
}
}
return result;
}
// 범위 내 숫자만 필터링
vector<int> filterRange(const vector<int>& numbers, int min, int max) {
vector<int> result;
for (int num : numbers) {
if (num >= min && num <= max) {
result.push_back(num);
}
}
return result;
}
// 중복 제거
vector<int> removeDuplicates(vector<int> numbers) {
sort(numbers.begin(), numbers.end());
auto it = unique(numbers.begin(), numbers.end());
numbers.erase(it, numbers.end());
return numbers;
}
// 벡터 출력 헬퍼 함수
void printVector(const string& label, const vector<int>& v) {
cout << label << ": ";
for (int num : v) {
cout << num << " ";
}
cout << endl;
}
int main() {
vector<int> numbers = {5, 2, 8, 1, 9, 3, 7, 2, 5, 8, 4, 6};
printVector("원본", numbers);
// 짝수만
vector<int> evens = filterEven(numbers);
printVector("짝수", evens);
// 3-7 범위
vector<int> ranged = filterRange(numbers, 3, 7);
printVector("3-7 범위", ranged);
// 중복 제거
vector<int> unique_nums = removeDuplicates(numbers);
printVector("중복 제거", unique_nums);
return 0;
}
설명: vector를 활용한 다양한 필터링 기법입니다. 조건에 맞는 요소만 추출하거나, 중복을 제거하는 등 실무에서 자주 사용하는 패턴입니다.
예시 3: 2차원 벡터로 게임 맵 구현
#include <iostream>
#include <vector>
using namespace std;
class GameMap {
private:
vector<vector<char>> map;
int rows, cols;
public:
GameMap(int r, int c) : rows(r), cols(c) {
// 빈 맵 초기화 ('.' = 빈 공간)
map.resize(rows, vector<char>(cols, '.'));
}
// 장애물 배치
void placeObstacle(int r, int c) {
if (isValid(r, c)) {
map[r][c] = '#';
}
}
// 플레이어 배치
void placePlayer(int r, int c) {
if (isValid(r, c) && map[r][c] == '.') {
map[r][c] = 'P';
}
}
// 아이템 배치
void placeItem(int r, int c) {
if (isValid(r, c) && map[r][c] == '.') {
map[r][c] = 'I';
}
}
// 맵 출력
void print() const {
cout << "\n=== 게임 맵 ===" << endl;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
cout << map[i][j] << " ";
}
cout << endl;
}
}
// 이동 가능 여부 체크
bool canMove(int r, int c) const {
return isValid(r, c) && map[r][c] != '#';
}
// 플레이어 이동
bool movePlayer(int fromR, int fromC, int toR, int toC) {
if (!isValid(fromR, fromC) || !isValid(toR, toC)) {
return false;
}
if (map[fromR][fromC] != 'P') {
cout << "플레이어가 없습니다" << endl;
return false;
}
if (!canMove(toR, toC)) {
cout << "이동할 수 없습니다" << endl;
return false;
}
// 아이템 획득
if (map[toR][toC] == 'I') {
cout << "아이템 획득!" << endl;
}
map[fromR][fromC] = '.';
map[toR][toC] = 'P';
return true;
}
private:
bool isValid(int r, int c) const {
return r >= 0 && r < rows && c >= 0 && c < cols;
}
};
int main() {
GameMap game(5, 8);
// 장애물 배치
game.placeObstacle(1, 2);
game.placeObstacle(1, 3);
game.placeObstacle(2, 3);
game.placeObstacle(3, 5);
// 아이템 배치
game.placeItem(1, 6);
game.placeItem(3, 2);
// 플레이어 배치
game.placePlayer(0, 0);
game.print();
// 플레이어 이동
cout << "\n플레이어 이동: (0,0) -> (0,1)" << endl;
game.movePlayer(0, 0, 0, 1);
game.print();
cout << "\n플레이어 이동: (0,1) -> (1,1)" << endl;
game.movePlayer(0, 1, 1, 1);
game.print();
return 0;
}
설명: 2차원 vector를 활용한 게임 맵 구현입니다. 동적으로 크기를 조절할 수 있으며, 장애물, 플레이어, 아이템 등을 자유롭게 배치할 수 있습니다.
자주 발생하는 문제
문제 1: reserve() vs resize() 혼동
증상: 메모리는 할당되었지만 접근 시 에러 발생
원인: reserve()와 resize()의 차이를 이해하지 못함
해결법:
// ❌ 잘못된 코드
vector<int> v;
v.reserve(100); // 메모리만 예약
v[0] = 10; // 에러! size()는 여전히 0
// ✅ 올바른 코드 (방법 1: resize 사용)
vector<int> v;
v.resize(100); // 크기 설정 + 0으로 초기화
v[0] = 10; // OK
// ✅ 올바른 코드 (방법 2: push_back 사용)
vector<int> v;
v.reserve(100); // 메모리 미리 할당 (재할당 방지)
for (int i = 0; i < 100; i++) {
v.push_back(i); // OK
}
차이점:
reserve(n): capacity만 증가 (size는 그대로)resize(n): size 변경 (필요시 capacity도 증가)
문제 2: 반복자 무효화 (Iterator Invalidation)
증상: 벡터 수정 후 반복자 사용 시 크래시
원인: push_back, erase 등으로 벡터가 재할당되면 기존 반복자가 무효화됨
해결법:
// ❌ 잘못된 코드
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();
v.push_back(6); // 재할당 발생 가능
cout << *it; // 크래시! it가 무효화됨
// ✅ 올바른 코드 (방법 1: 인덱스 사용)
vector<int> v = {1, 2, 3, 4, 5};
int idx = 0;
v.push_back(6);
cout << v[idx]; // OK
// ✅ 올바른 코드 (방법 2: reserve로 재할당 방지)
vector<int> v = {1, 2, 3, 4, 5};
v.reserve(100); // 충분한 공간 확보
auto it = v.begin();
v.push_back(6); // 재할당 없음
cout << *it; // OK
// ❌ erase 후 반복자 사용
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); it++) {
if (*it == 3) {
v.erase(it); // it 무효화!
// it++ 하면 크래시
}
}
// ✅ 올바른 코드
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ) {
if (*it == 3) {
it = v.erase(it); // erase가 다음 반복자 반환
} else {
it++;
}
}
문제 3: 불필요한 복사로 인한 성능 저하
증상: 벡터를 함수에 전달할 때 프로그램이 느려짐
원인: 값 전달로 인한 전체 벡터 복사
해결법:
// ❌ 느린 코드 (전체 복사)
void processVector(vector<int> v) { // 복사 발생!
for (int x : v) {
cout << x << " ";
}
}
int main() {
vector<int> v(1000000); // 100만 개
processVector(v); // 100만 개 전체 복사!
}
// ✅ 빠른 코드 (const 참조)
void processVector(const vector<int>& v) { // 복사 없음
for (int x : v) {
cout << x << " ";
}
}
// ✅ 수정이 필요한 경우 (참조)
void modifyVector(vector<int>& v) { // 복사 없음
for (int& x : v) {
x *= 2;
}
}
// ✅ 소유권 이전 (move)
vector<int> createLargeVector() {
vector<int> v(1000000);
// ... 초기화 ...
return v; // 보통 NRVO로 복사·이동 자체가 생략되고, 안 되면 이동
}
값 전달과 참조 전달의 차이는 벡터 크기에 비례해 벌어집니다. 100만 개짜리 벡터를 값으로 넘기면 호출할 때마다 힙 할당 한 번과 100만 개 원소 복사가 일어나지만, const&로 넘기면 주소 하나만 전달되므로 원본 크기와 무관합니다. 반대로 반환할 때는 return v;처럼 값으로 돌려주는 것이 맞습니다. 컴파일러가 반환값 최적화(NRVO)로 복사를 없애고, 최악의 경우에도 포인터 세 개만 옮기는 이동이 일어나기 때문입니다. return std::move(v);처럼 굳이 move를 붙이면 오히려 NRVO를 막아 손해이고, Clang은 -Wpessimizing-move 경고를 냅니다.
용량 증가 방식과 성능
push_back이 상환 O(1)인 이유는 용량이 부족할 때 일정 배율로 늘리기 때문입니다. libstdc++와 libc++는 2배, MSVC는 1.5배로 늘리는 것으로 알려져 있으며, 재할당 때마다 새 메모리를 잡고 기존 원소를 모두 옮긴 뒤 옛 메모리를 해제합니다. 원소 개수를 미리 안다면 reserve(n)으로 재할당을 한 번으로 줄이는 것이 가장 간단한 최적화입니다.
재할당 때 원소를 “옮기는” 방식에도 함정이 있습니다. 사용자 정의 타입의 이동 생성자에 noexcept가 없으면, vector는 예외 안전성을 지키기 위해 이동 대신 복사를 선택합니다(std::move_if_noexcept). 그래서 문자열을 멤버로 가진 클래스의 이동 생성자를 직접 만들면서 noexcept를 빠뜨리면, 벡터가 커질 때마다 조용히 전체 복사가 일어납니다. 이동 생성자를 직접 쓸 때는 noexcept를 붙이는 습관이 필요합니다.
용량은 clear()나 erase로 원소를 지워도 줄어들지 않습니다. 한때 크게 커졌던 벡터의 메모리를 돌려받고 싶다면 shrink_to_fit()을 호출할 수 있지만, 이는 구현이 무시해도 되는 요청일 뿐이고 실제로 줄이면 재할당이 한 번 일어납니다. 마지막으로 vector<bool>은 공간 절약을 위해 비트 단위로 압축된 특수화라, bool& r = vb[0];이 컴파일되지 않고 data()도 없습니다. 진짜 bool 배열이 필요하면 vector<char>나 std::deque<bool>을 쓰는 편이 낫습니다.
같이 보면 좋은 글
- C++ std::string 자주 쓰는 함수 정리
- C++ set vs unordered_set
- C++ map·unordered_map·flat_map
- C++ vector vs list vs deque
- C++ 배열 vs vector