[C++] vector 완벽 정복: 기초부터 핵심 사용법, 장단점까지 한눈에!
C++ 프로그래밍에서 데이터를 유연하게 관리하고 싶을 때 가장 먼저 떠오르는 컨테이너 중 하나가 바로 std::vector입니다. C++ 표준 템플릿 라이브러리(STL)의 핵심 구성 요소인 vector는 동적으로 크기가 조절되는 배열로, 편리함과 강력한 기능을 제공합니다. 이 글에서는 C++ vector의 기본적인 사용법부터 주요 기능, 그리고 장단점까지 자세히 알아보겠습니다.
1. C++ vector란 무엇인가?
std::vector는 동일한 타입의 객체들을 연속된 메모리 공간에 저장하는 시퀀스 컨테이너입니다. 마치 자동으로 크기가 조절되는 배열과 같다고 생각할 수 있습니다.
1.1. 특징
- 동적 메모리 할당:
vector를 생성하면 내부적으로 힙(heap) 메모리에 데이터가 저장됩니다. 이로 인해 프로그램 실행 중에 크기를 늘리거나 줄일 수 있는 유연성을 갖습니다.
1.2. 장점
자동 메모리 관리: 요소 추가 시 자동으로 메모리가 확장되고, 소멸 시 자동으로 해제되어 메모리 누수 걱정을 덜 수 있습니다.
다양한 멤버 함수: 원소 접근, 추가, 삭제, 크기 확인 등 다양한 편의 기능을 제공합니다.
임의 접근 가능: 인덱스를 통해 특정 위치의 원소에 빠르게 접근할 수 있습니다 (
O(1)시간 복잡도).다른 STL 알고리즘과의 호환성:
<algorithm>헤더의 다양한 함수들과 쉽게 연동하여 사용할 수 있습니다.
1.3. 단점
배열 대비 약간의 성능 저하: 단순 접근 속도는 C-스타일 배열보다 약간 느릴 수 있습니다.
재할당 비용:
vector의 용량(capacity)을 초과하여 원소를 추가할 경우, 새로운 더 큰 메모리 공간을 할당하고 기존 원소들을 복사/이동하는 재할당(reallocation) 과정에서 성능 저하가 발생할 수 있습니다.중간 삽입/삭제 비용:
vector의 중간에 원소를 삽입하거나 삭제할 경우, 해당 지점 이후의 모든 원소들을 이동시켜야 하므로O(N)의 시간 복잡도를 가집니다.
2. Vector 생성 및 초기화
vector를 사용하기 위해서는 먼저 <vector> 헤더 파일을 포함해야 합니다.
#include <vector>
#include <iostream> // 예제 출력을 위해 사용
int main() {
// 1. 빈 vector 생성
std::vector<int> v1;
// 2. 특정 크기로 생성 (모든 원소는 0 또는 기본값으로 초기화)
std::vector<int> v2(5); // 크기가 5인 int형 vector, 모든 원소는 0으로 초기화
// 3. 특정 크기로 생성하며 특정 값으로 초기화
std::vector<int> v3(5, 100); // 크기가 5인 int형 vector, 모든 원소를 100으로 초기화
// 4. 중괄호 초기화 (Initializer List, C++11 이상)
std::vector<int> v4 = {1, 2, 3, 4, 5};
std::vector<std::string> v_str = {"apple", "banana", "cherry"};
// 5. 다른 vector로부터 복사하여 생성
std::vector<int> v5 = v4; // v4의 모든 원소를 복사하여 v5 생성
std::vector<int> v6(v4); // 위와 동일
// 6. 2차원 vector 생성 (행과 열 모두 가변)
std::vector<std::vector<int>> matrix1; // 빈 2차원 vector
std::vector<std::vector<int>> matrix2(3, std::vector<int>(4, 0)); // 3x4 크기의 2차원 vector, 모든 원소 0으로 초기화
// 7. assign() 함수를 이용한 초기화 또는 재할당
std::vector<int> v7;
v7.assign(5, 7); // v7에 5개의 7을 할당 (output: 7 7 7 7 7)
return 0;
}
3. Vector 원소 접근
vector의 원소에 접근하는 주요 방법은 다음과 같습니다.
operator[]: 배열처럼 인덱스를 사용하여 원소에 접근합니다. 범위를 벗어난 인덱스 접근 시 정의되지 않은 행동(undefined behavior)을 유발하므로 주의해야 합니다.at(): 인덱스를 사용하여 원소에 접근합니다. 범위를 벗어난 인덱스 접근 시std::out_of_range예외를 발생시켜 더 안전합니다.front(): 첫 번째 원소에 대한 참조를 반환합니다. 비어있는vector에 사용 시 정의되지 않은 행동을 유발합니다.back(): 마지막 원소에 대한 참조를 반환합니다. 비어있는vector에 사용 시 정의되지 않은 행동을 유발합니다.
#include <vector>
#include <iostream>
#include <stdexcept> // std::out_of_range 사용
int main() {
std::vector<int> v = {10, 20, 30, 40, 50};
std::cout << "v[1]: " << v[1] << std::endl; // output: 20
std::cout << "v.at(2): " << v.at(2) << std::endl; // output: 30
std::cout << "v.front(): " << v.front() << std::endl; // output: 10
std::cout << "v.back(): " << v.back() << std::endl; // output: 50
try {
std::cout << v.at(10) << std::endl; // 범위를 벗어난 접근
} catch (const std::out_of_range& oor) {
std::cerr << "Out of Range error: " << oor.what() << std::endl;
}
return 0;
}
operator[]는 속도가 약간 빠르지만 안전성이 떨어지고, at()은 안전하지만 예외 처리 비용이 발생할 수 있습니다. 따라서 접근하려는 인덱스가 유효하다는 것이 확실할 때는 operator[]를, 그렇지 않다면 at()을 사용하는 것이 좋습니다.
4. Vector 반복자 (Iterators)
반복자(iterator)는 vector 내부의 원소들을 가리키는 객체로, 포인터와 유사하게 동작합니다. vector의 원소들을 순회하거나 특정 알고리즘에 전달할 때 사용됩니다.
begin():vector의 첫 번째 원소를 가리키는 반복자를 반환합니다.end():vector의 마지막 원소 바로 다음 위치(존재하지 않는 원소)를 가리키는 반복자를 반환합니다.end()가 가리키는 위치는 실제 원소가 아니므로 역참조하면 안 됩니다.rbegin():vector의 마지막 원소를 가리키는 역반복자(reverse iterator)를 반환합니다. (역순 순회의 시작점)rend():vector의 첫 번째 원소 바로 앞 위치를 가리키는 역반복자를 반환합니다. (역순 순회의 끝점)
#include <vector>
#include <iostream>
#include <algorithm> // for_each 사용
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
// 일반 반복자를 사용한 순회
std::cout << "Forward iteration: ";
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " "; // output: 1 2 3 4 5
}
std::cout << std::endl;
// 범위 기반 for문 (C++11 이상, 내부적으로 반복자 사용)
std::cout << "Range-based for loop: ";
for (int val : v) {
std::cout << val << " "; // output: 1 2 3 4 5
}
std::cout << std::endl;
// 역반복자를 사용한 순회
std::cout << "Reverse iteration: ";
for (std::vector<int>::reverse_iterator rit = v.rbegin(); rit != v.rend(); ++rit) {
std::cout << *rit << " "; // output: 5 4 3 2 1
}
std::cout << std::endl;
return 0;
}
5. Vector 원소 추가 및 삭제
vector는 다양한 방법으로 원소를 추가하거나 삭제할 수 있는 멤버 함수를 제공합니다.
push_back(value):vector의 맨 뒤에 원소를 추가합니다.pop_back():vector의 맨 뒤 원소를 제거합니다. 비어있는vector에 사용 시 정의되지 않은 행동을 유발합니다.insert(position, value): 지정된position(반복자) 앞에value를 삽입합니다.insert(position, count, value): 지정된position앞에value를count개 삽입합니다.insert(position, first, last): 지정된position앞에 다른 컨테이너의[first, last)범위의 원소들을 삽입합니다.emplace_back(args...):vector의 맨 뒤에 원소를 생성하여 추가합니다.push_back과 달리 객체를 직접 생성하므로 임시 객체 생성 및 복사/이동 비용을 줄일 수 있습니다 (C++11 이상).emplace(position, args...): 지정된position앞에 원소를 생성하여 삽입합니다 (C++11 이상).erase(position): 지정된position(반복자)의 원소를 제거합니다.erase(first, last):[first, last)범위의 원소들을 제거합니다.clear():vector의 모든 원소를 제거합니다 (size는 0이 되지만 capacity는 유지될 수 있습니다).resize(new_size):vector의 크기를new_size로 변경합니다. 크기가 커지면 기본값으로 초기화된 원소가 추가되고, 작아지면 뒤쪽 원소들이 제거됩니다.swap(other_vector): 현재vector와other_vector의 내용을 교환합니다.
5.1. push_back vs emplace_back
push_back은 이미 생성된 객체를 vector로 복사하거나 이동시킵니다. 반면 emplace_back은 vector 내부 메모리 공간에 직접 객체를 생성합니다.
struct MyData {
int id;
std::string name;
MyData(int i, std::string s) : id(i), name(s) {
std::cout << "MyData constructor called for " << name << std::endl;
}
MyData(const MyData& other) : id(other.id), name(other.name) {
std::cout << "MyData copy constructor called for " << name << std::endl;
}
MyData(MyData&& other) noexcept : id(other.id), name(std::move(other.name)) {
std::cout << "MyData move constructor called for " << name << std::endl;
}
};
std::vector<MyData> vec;
MyData obj1(1, "obj1");
std::cout << "--- push_back ---" << std::endl;
vec.push_back(obj1); // 복사 생성자 호출 (또는 경우에 따라 이동 생성자)
std::cout << "--- emplace_back ---" << std::endl;
vec.emplace_back(2, "obj2"); // 생성자 직접 호출. 불필요한 임시 객체 생성 및 복사/이동 방지
emplace_back (및 emplace)은 인자를 받아 vector 내부에서 직접 객체를 생성하므로, 특히 복잡한 객체의 경우 복사/이동 생성자 호출을 줄여 성능상 이점을 가질 수 있습니다.
6. Vector 용량 관리 (Capacity Management)
vector는 내부적으로 실제 저장된 원소의 개수(size)와 할당된 메모리 공간의 크기(capacity)를 관리합니다.
size():vector에 저장된 원소의 개수를 반환합니다.capacity():vector에 재할당 없이 저장할 수 있는 최대 원소의 개수를 반환합니다.capacity()는 항상size()보다 크거나 같습니다.empty():vector가 비어있는지 (size가 0인지) 여부를 반환합니다 (true/false).max_size(): 시스템이vector에 할당할 수 있는 이론적인 최대 원소 수를 반환합니다.reserve(new_cap):vector의capacity를 최소new_cap으로 설정합니다. 만약new_cap이 현재capacity보다 작으면 아무 동작도 하지 않을 수 있습니다 (C++ 표준에 따라 다름). 이 함수는 원소를 많이 추가할 것이 예상될 때 미리 공간을 확보하여 재할당 횟수를 줄이는 데 유용합니다.shrink_to_fit():vector의capacity를size에 맞게 줄이도록 요청합니다 (C++11 이상). 메모리 사용량을 줄이고 싶을 때 사용하지만, 반드시 요청이 받아들여지는 것은 아닙니다.
#include <vector>
#include <iostream>
int main() {
std::vector<int> v;
std::cout << "Initial size: " << v.size() << ", capacity: " << v.capacity() << std::endl;
v.reserve(10); // 미리 용량을 10으로 확보
std::cout << "After reserve(10) -> size: " << v.size() << ", capacity: " << v.capacity() << std::endl;
for (int i = 0; i < 5; ++i) {
v.push_back(i);
std::cout << "push_back(" << i << ") -> size: " << v.size() << ", capacity: " << v.capacity() << std::endl;
}
// output: (컴파일러 및 환경에 따라 capacity 증가 방식이 다를 수 있음)
// push_back(0) -> size: 1, capacity: 10
// ...
// push_back(4) -> size: 5, capacity: 10
v.shrink_to_fit(); // 사용하지 않는 여유 공간을 해제하도록 요청
std::cout << "After shrink_to_fit() -> size: " << v.size() << ", capacity: " << v.capacity() << std::endl;
// output: After shrink_to_fit() -> size: 5, capacity: 5 (보장되지는 않음)
v.clear(); // 모든 원소 제거
std::cout << "After clear() -> size: " << v.size() << ", capacity: " << v.capacity() << std::endl;
// output: After clear() -> size: 0, capacity: 5 (capacity는 유지될 수 있음)
// capacity까지 완전히 해제하려면 (swap trick)
std::vector<int>().swap(v);
std::cout << "After swap trick -> size: " << v.size() << ", capacity: " << v.capacity() << std::endl;
// output: After swap trick -> size: 0, capacity: 0
return 0;
}
vector의 size가 capacity를 초과하면, 더 큰 메모리 블록이 할당되고 모든 기존 원소들이 새 위치로 복사(또는 이동)됩니다. 이 재할당 과정은 비용이 크므로, reserve()를 적절히 사용하면 성능을 향상시킬 수 있습니다.
7. Vector 사용 시 주의사항
- 중간 삽입/삭제 성능:
vector의 중간에 원소를 삽입하거나 삭제하는 작업은 그 뒤의 모든 원소를 이동시켜야 하므로 성능에 부담을 줄 수 있습니다. 이러한 작업이 빈번하다면std::list나std::deque같은 다른 컨테이너를 고려하는 것이 좋습니다. - 반복자 무효화:
vector의 특정 연산(예:push_back으로 인한 재할당,insert,erase)은 기존 반복자를 무효화시킬 수 있습니다. 무효화된 반복자를 사용하면 예기치 않은 동작이나 프로그램 충돌을 일으킬 수 있으므로 주의해야 합니다. clear()와 메모리:clear()함수는vector의 모든 원소를 제거하여size()를 0으로 만들지만,capacity()는 변경하지 않을 수 있습니다. 즉, 할당된 메모리는 그대로 남아있을 수 있습니다. 메모리까지 완전히 해제하고 싶다면 위에서 언급된 "swap trick" (std::vector<T>().swap(my_vector))을 사용하거나, C++11 이상에서는shrink_to_fit()을 사용할 수 있습니다.
8. 결론
std::vector는 C++에서 가장 유용하고 널리 사용되는 컨테이너 중 하나입니다. 동적 크기 조절, 편리한 멤버 함수, 다른 STL 구성 요소와의 호환성 등 많은 장점을 제공합니다. 하지만 재할당 비용, 중간 삽입/삭제 성능, 반복자 무효화와 같은 특징들을 잘 이해하고 사용해야 최적의 성능을 얻을 수 있습니다. 상황에 맞는 적절한 사용법을 익힌다면 vector는 여러분의 C++ 프로그래밍을 더욱 강력하고 효율적으로 만들어 줄 것입니다.그래밍을 더욱 강력하고 효율적으로 만들어 줄 것입니다.