원시 배열 대신 vector를 쓰는 이유
원시 배열은 컴파일 타임에 고정된 크기를 가지며, 함수에 전달되는 순간 자신의 길이를 잊어버립니다. std::vector는 두 문제를 모두 해결합니다. 자신의 길이를 추적하고, 필요할 때 커지며, 자신의 메모리를 스스로 정리하는 크기 조정 가능한 배열입니다. 모던 C++에서 vector는 기본 컨테이너입니다. 굳이 그러지 말아야 할 구체적인 이유가 있을 때만 원시 배열에 손을 뻗으세요.
<vector>를 포함한 다음, 요소 타입을 꺾쇠괄호 안에 넣어 선언합니다:
scores.size()는 항상 현재 길이를 알려 줍니다. 따로 동기화해야 할 int n도 필요 없고 sizeof 트릭도 필요 없습니다. {90, 75, 100, 60}은 중괄호 초기화자이며, vector는 네 개의 자리가 필요하다는 것을 스스로 알아냅니다.
vector 생성과 초기화
미리 무엇을 알고 있느냐에 따라, 만드는 방법은 여러 가지입니다:
소괄호 대 중괄호 함정에 주의하세요. vector<int> tens(5, 10)은 10의 복사본을 다섯 개 만들지만, vector<int> tens{5, 10}은 5와 10을 담은 두 요소짜리 vector를 만듭니다. 소괄호는 "크기와 채울 값"을 뜻하고, 중괄호는 "이 리터럴 요소들"을 뜻합니다.
요소 추가와 제거
vector의 핵심은 커진다는 것입니다. push_back은 끝에 추가하고, pop_back은 끝에서 제거합니다:
back()은 마지막 요소를, front()는 첫 요소를 반환합니다. v[v.size() - 1]이나 v[0]보다 깔끔합니다. C++11부터는 요소를 제자리에서 생성하는 emplace_back(args...)도 쓸 수 있으며, 무거운 타입에서는 임시 복사를 피할 수 있습니다.
흔한 초보자의 실수는 빈 vector에 대해 front()나 back()을 호출하는 것입니다. 이는 오류가 아니라 정의되지 않은 동작이므로, 항상 먼저 if (!v.empty())로 방어하세요.
요소 읽기: [] 대 at()
vector는 배열과 똑같이 []로 인덱싱합니다. 하지만 []는 경계 검사를 하지 않습니다. 범위를 벗어난 인덱스는 정의되지 않은 동작이며, 조용히 쓰레기 값을 읽거나 나중에 엉뚱한 곳에서 크래시할 수 있습니다:
vector<int> v = {1, 2, 3};
cout << v[10]; // 정의되지 않은 동작 - 검사도, 오류도 없음
안전을 원한다면 at()을 사용하세요. 인덱스를 검사하고 잘못된 접근에서 std::out_of_range를 던지므로, 손상 대신 명확한 실패를 얻습니다:
경험칙: 인덱스가 유효함을 이미 입증한 빡빡한 루프에서는 []를, 잘못된 입력이 끼어들 수 있는 경계에서는 at()을 사용하세요.
vector 순회하기
vector를 훑는 가장 깔끔한 방법은 범위 기반 for 루프입니다. 복사 없이 읽으려면 요소를 const auto&로 받고, 제자리에서 수정하려면 auto&로 받으세요:
정말로 인덱스가 필요하다면(예: 이웃끼리 비교) 전통적인 카운팅 루프를 쓰세요. 다만 size()는 부호 없는 타입(size_t)을 반환한다는 점에 유의하세요. 부호 있는 int i와 비교하면 컴파일러 경고와 의외의 랩어라운드가 발생할 수 있으니, 가능하면 size_t i나 범위 기반 루프를 선호하세요:
for (size_t i = 0; i < v.size(); i++) { // int가 아니라 size_t
cout << v[i];
}
size, capacity, reserve
vector는 두 가지 숫자를 유지합니다. size()(담고 있는 요소 수)와 capacity()(커지지 않고 담을 수 있는 요소 수)입니다. push_back이 용량을 초과하면 vector는 더 큰 블록을 할당하고, 모든 요소를 복사한 뒤, 옛 블록을 해제합니다. 반복적인 push_back이 분할 상환 기준으로 저렴하면서도 개별 재할당은 공짜가 아닌 이유가 바로 이것입니다:
추가할 요소 수를 대략 안다면, 반복 재할당을 건너뛰기 위해 먼저 reserve()를 호출하세요. reserve()는 크기가 아니라 용량을 바꾼다는 점에 유의하세요. 요소를 push하기 전까지 vector의 요소 수는 여전히 0입니다.
이 재할당은 vector에서 가장 고약한 버그의 근원이기도 합니다. 커지면서 저장소가 옮겨지기 때문에, vector 안으로 저장해 둔 포인터, 참조, 반복자는 재할당을 일으키는 push_back 이후 댕글링이 됩니다:
vector<int> v = {1, 2, 3};
int& first = v[0]; // vector 안으로의 참조
v.push_back(4); // 재할당할 수 있음...
cout << first; // 댕글링 - 해제된 메모리를 가리킬 수 있음
반복자에도 똑같이 적용됩니다. 저장해 둔 반복자로 순회하는 동안 push_back이나 erase를 하지 마세요. 루프 중에 항목을 제거해야 한다면, erase의 반환값을 쓰거나 std::remove와 함께 erase-remove 관용구를 사용하세요.
다음: map
vector는 무언가를 위치로 찾을 때(요소 0, 요소 1 등) 완벽합니다. 하지만 종종 무언가를 키로 찾고 싶을 때가 있습니다. 사용자 이름, 상품 ID, 단어 같은 것이죠. 그것이 바로 std::map의 용도입니다. 다음으로 C++의 키-값 컨테이너인 map을 다루면서, 항목을 삽입하고 조회하고 순회하는 방법, 그리고 거의 모두가 걸려 넘어지는 "[]가 기본값을 만들어 버리는" 함정을 살펴보겠습니다.
자주 묻는 질문
C++에서 vector란 무엇인가요?
std::vector는 C++ 표준 라이브러리에서 제공하는 동적(크기 조정 가능) 배열입니다. 원시 배열과 달리 자신의 크기를 알고 있고, push_back으로 요소를 추가하면 자동으로 커지며, 메모리도 알아서 해제해 줍니다. <vector>를 포함하고 vector<int> v;라고 작성하면 하나 만들 수 있습니다.
C++ vector에서 []와 at()의 차이는 무엇인가요?
v[i]는 경계 검사를 하지 않습니다 - 범위를 벗어난 인덱스는 정의되지 않은 동작(크래시 또는 조용한 메모리 손상)입니다. v.at(i)는 인덱스를 검사하고 유효하지 않으면 std::out_of_range를 던집니다. 이미 인덱스를 검증한 핫 루프에서는 []를, 안전하고 디버깅하기 좋은 실패가 필요할 때는 at()을 사용하세요.
push_back은 C++ vector 안을 가리키는 포인터와 참조를 무효화하나요?
네, 그럴 수 있습니다. vector의 용량이 다 차면 push_back은 저장소를 새 블록으로 재할당하는데, 이때 기존 요소를 가리키던 모든 포인터, 참조, 반복자가 무효화됩니다. push_back을 사이에 두고 요소에 대한 참조를 들고 있지 마세요. 그리고 가능하다면 미리 reserve()를 호출해 예상치 못한 재할당을 피하세요.