벡터(Vector)

벡터(Vector)는 C++에서 가장 많이 사용되는 STL(Standard Template Library)에 있는 컨테이너 중 하나로
동적 베열, 즉 크기를 동적으로 조절할 수 있는 데이터 구조이다.

1. 벡터 설명

벡터의 특징에 대해서 몇 가지 알아보도록 하겠다.

  • 벡터는 배열과 비슷하지만, 배열과 다르게 크기를 미리 지정하지 않고 동적으로 조절 가능하다.
  • 다른 STL과 마찬가지로 template를 사용하기에 모든 데이터 타입이 들어올 수 있다.
  • 속도적인 측면에서 배열보다 느리지만, 메모리를 효율적으로 관리할 수 있다는 장점이 있다.
  • 벡터를 생성하면 메모리 heap에 생성되며 동적 할당된다.

2. 벡터 생성

벡터를 생성하는 방법은 다양하다. 아래 코드와 주석으로 다양한 벡터의 생성 예시를 작성하였다.

#include <vector> // 벡터 STL을 추가

vector<int> v; // vector<자료형> 변수명

vector<int> v(5); // 벡터 v를 생성 이후 크기를 4로 할당한 후 모두 0으로 초기화

vector<int> v = {1, 2, 3, 4}; // 벡터 생성, 원소 4개 공간 할당 후 1, 2, 3, 4로 각각 초기화

vector<vector<int>> v; // 2차원 벡터 생성


3. 벡터 Iterators

벡터는 다양한 iterator를 가진다. iterators는 벡터를 반복하는데 사용할 수 있다.

vector<int> v = {1,2,3,4,5};
vector<int>::iterator iter; // int형 벡터의 이터레이터 iter 선언
for (iter = v.begin(); iter != v.end(); iter++){
// v.begin() => 벡터 시작점의 주소값 반환 v.end() => 벡터 (끝 부분 + 1) 주소값 반환
    cout << *iter << " "; // 이터레이터는 주소값이 담기기에 *를 사용
    // output: 1 2 3 4 5
}
cout << endl;
vector<int> v = {1,2,3,4,5};
vector<int>::reverse_iterator iter; // rbegin과 rend를 변수에 초기화하기 위해서 reverse_iterator 사용
for (iter = v.rbegin(); iter != v.rend(); iter++){
// v.rbegin() => 벡터의 마지막을 시작점으로 반환 v.rend() => 벡터의 시작+1 지점을 끝 부분으로 반환
    cout << *iter << " ";
    // output: 5 4 3 2 1
}


4. 벡터의 요소 접근

벡터의 요소를 접근하는 다양한 방법에 대해 알아보자

vector<int> v = {1, 2, 3, 4, 5};
cout << v.at(1) << endl; // 벡터의 1번째 요소 반환, 범위 검사를 한다.
cout << v[1] << endl; // 벡터의 1번째 요소 반환, 범위 검사 x
cout << v.front() << " " << v.back() << endl; // v.front() => 벡터의 첫 요소 반환 v.back() => 벡터의 마지막 요소 반환
/* output
2
2
1 5
*/

벡터의 at()과 []의 차이점은 범위 검사의 여부이다.

  • at()은 범위를 검사하기에 범위 밖 요소에 접근 시 예외처리를 발생시킨다.
  • []은 범위 검사를 하지 않기에 범위 밖 요소에 접근 시 일반적인 디버깅이 발생한다.

    보통 벡터는 효율을 중점으로 둔 라이브러리 함수이기에 []를 권장한다.



5. 벡터 요소 삽입 / 삭제

벡터의 요소를 삽입하고 삭제하는 다양한 메소드들에 대하여 알아보자

vector<int> v = {1, 2, 3, 4, 5};
v.push_back(6);
v.push_back(7); // push_back() => 벡터의 마지막 부분에 요소 추가
v.insert(v.begin()+2, 9);  // insert(위치 주소, 삽입할 요소) => 위치에 요소 삽입
v.emplace(v.begin()+2, 10); // emplace(위치 주소, 삽입할 요소) => 위치에 요소 삽입
v.emplace_back(16); // emplace_back() => 마지막 부분에 요소 추가
for (int i : v){ cout << i << " ";} // 1 2 10 9 3 4 5 6 7 16
cout << endl;
v.erase(v.begin()+2); // erase(위치 주소) => 위치에 요소 제거
for (int i : v){ cout << i << " ";} // 1 2 9 3 4 5 6 7 16
cout << endl;
v.erase(v.begin()+2, v.end()-1); // erase(위치 주소 A, 위치 주소 B) => A부터 B-1까지 제거
for (int i : v){ cout << i << " ";} // 1 2 16
cout << endl;
v.clear(); // clear() => 전체 요소 제거
v.push_back(1); 
for (int i : v){ cout << i << " ";} // 1
cout << endl;
v.resize(5); // resize() => 크기 조정, 조정된 값이 기존 크기보다 클 경우 0으로 초기화
for (int i : v){ cout << i << " ";} // 1 0 0 0 0
cout << endl;
emplace와 insert의 차이는 뭘까? / emplace_back과 push_back의 차이는?

기본적으로 push_back은 값을 넣는 과정에서 복사 생성자를 호출한다.
또한 insert는 모든 값들을 새로운 메모리에 복사한 후 해당 자리에 값을 넣게 된다.
( -> 일반적인 배열에서 요소를 중간 삽입하는 것처럼 )
복사 생성자로 인해 오버헤드가 커지게 되며, 이는 곳 성능 저하를 야기한다. => emplace, emplace_back으로 해결

emplace와 emplace_back은 벡터 내부에서 값들을 생성한다.
class test{
private:
    int num;
    string str;
public:
    test(int i, string s) : num(i), str(s) {}
};
vector<test> v;

test t1(1, "jynoh");
v.push_back(t1);
v.push_back(2, "jyn"); // 잘못된 코드, 코드 실행 시 제외시킬 것
v.emplace_back(3, "njy");

cout << v[1].getNum() << endl; // 3

push_back함수는 내부적으로 탬플릿에 대한 생성자 호출을 지원하지 않기에
push_back(2, “jyn”)과 같은 코드는 디버깅 오류가 나타난다.
따라서 v.push_back(t1)처럼 미리 선언된 변수를 인자로 넣게 되며 이를 넣을 때 복사 생성자가 호출된다.

emplace_back을 사용하면 내부적으로 탬플릿에 대한 생성자 호출을 지원하기에
복사 생성자의 호출없이 입력이 가능하다.

6. 벡터 size, capacity

size는 벡터의 현재 원소 개수를 의미한다, capacity는 최대 할당 크기로
size가 capacity를 넘게 될 경우, 재할당이 발생한다.


재할당이 발생하면 모든 값들을 새로운 메모리 공간에 복사한 후, 기존 벡터를 제거하는데
이런 복사 과정에서 역시 복사 생성자가 발생한다. 이는 프로그램의 퍼포먼스 저하를 야기한다.

vector<int> v;
for (int i = 1; i <= 10; i++){
    v.push_back(i);
    cout << "{" << v.size() << " " << v.capacity() << "} ";
}
cout << endl;

위 코드의 실행 결과는 아래와 같은데

{1 1} {2 2} {3 4} {4 4} {5 8} {6 8} {7 8} {8 8} {9 16} {10 16}
  • size가 capacity를 넘어갈 경우 capacity의 비트 수가 1커져 2의 n제곱 형식으로 되는 것으로 보인다.
  • push_back에서 2, 3, 5, 9를 추가할 때 벡터의 메모리 복사가 일어난다는 것을 알 수 있다.
  • 벡터의 capacity는 같은 코드라도 컴파일 환경에 따라 달라질 수 있다.

7. 벡터 reserve, shrink_to_fit, swap

벡터의 메모리 재할당이나 공간을 효율적으로 사용하기 위해

  • reserve
  • shrink_to_fit
  • swap

등의 메소드가 사용된다.

vector<int>v = { 1, 2, 3, 4, 5, 6, 7, 8, 9};

cout << v.size() << endl;    // 9
cout << v.capacity() << endl; // 9

v.reserve(120); // 벡터의 capacity를 지정, 인자에 현 capacity보다 적은 값이 들어가면 현 capacity 유지
cout << v.capacity() << endl; // 120

v.shrink_to_fit(); // 벡터의 capacity를 size에 맞게
cout << v.capacity() << endl; // 9

cout << v.empty() << endl; // 0
v.clear();
cout << v.empty() << endl; // 1
cout << v.capacity() << endl; // 9 clear로 원소를 모두 제거하여도 capacity는 유지된다.
vector<int>().swap(v); // 빈 벡터 공간과 swap을 통해 capacity를 0으로 바꾸는 방법
cout << v.capacity() << endl; // 0




이상으로 벡터 라이브러리의 간단한 기능, 사용법에 대하여 알아보았다.
벡터를 사용할 때, 재할당에 주의하여 요소를 추가할 것
emplace, empalce_back은 복사 생성자를 호출하지 않기에 퍼포먼스 향상에 도움