07
07
728x90

vector

vector는 배열과 거의 비슷하지만 필요한 경우 메모리를 자동으로 동적으로 할당해 준다.

배열 동적할당의 상위호환이라고 볼 수 있다.

하지만 이는 시퀀스 컨테이너의 일종으로 원소를 순차적으로 탐색하기에 검색을 사용하며 속도가 중요한 경우에는 적합하지 않다.


header

#include <vector>

vector는 위의 구문을 추가해야 사용할 수 있다.

 

선언

vector<int> v1;
vector<int> v2(3);
vector<int> v3(3, 1);
vector<int> v4 = { 1, 2, 3 };

vector<타입> 이름;의 형태로 선언한다.

첫 번째 방법은 벡터를 선언하는 가장 기본적인 방법이다.

두 번째 방법은 벡터를 선언하며 원소를 3개 담을 수 있게 선언과 초기 할당을 같이 해 주는 방법이다.

세 번째 방법은 벡터를 선언해 초기 할당을 하면서 값을 1로 초기화한다.

네 번째 방법은 원소를 지정해서 그 크기만큼 초기 할당까지 하는 것이다.

 

설명

#include <iostream>
#include <vector>

using namespace std;

int main() {
    // int형을 저장하는 vector
    vector<int> v1;     // 이렇게 초기화 하면 필요한 경우 할당한 메모리를 자동으로 늘림

    // 처음 할당한 만큼의 2배씩 메모리를 더 할당한다.
    // vector.size()는 vector가 가진 원소의 갯수
    // vector.capacity()는 vector가 할당한 메모리의 수(비었어도 체크)
    cout << v1.size() << " / " << v1.capacity() << '\n';

    // 한 개 들어가서 메모리 할당 1만큼 받음
    v1.push_back(1);
    cout << v1.size() << " / " << v1.capacity() << '\n';
    cout << "address: " << &v1[0] << '\n';

    // 두 개 원소를 담아야 하므로 메모리 할당 1*2로 늘림
    v1.push_back(2);
    cout << v1.size() << " / " << v1.capacity() << '\n';
    cout << "address: " << &v1[0] << '\n';

    // 세 개 있으므로 할당 1*(2^2)가 됨
    v1.push_back(3);
    cout << v1.size() << " / " << v1.capacity() << '\n';
    cout << "address: " << &v1[0] << '\n';

    // 네 개 있으므로 할당 안늘어나고 그대로 1*(2^2)
    v1.push_back(4);
    cout << v1.size() << " / " << v1.capacity() << '\n';   
    cout << "address: " << &v1[0] << '\n';

    // 다섯 개가 되어서 1*(2^3)로 할당 늘림
    v1.push_back(5);
    cout << v1.size() << " / " << v1.capacity() << '\n';
    cout << "address: " << &v1[0] << '\n';

    return 0;
}

위는 첫 번째 방법으로 벡터를 선언한 모습이다.

참고 사항으로 vector의 size()는 벡터가 담고있는 원소의 갯수를 보여주고, capacity()는 벡터가 할당한 담을 수 있는 원소의 갯수를 보여준다. 즉 capacity()는 원소를 담고있지 않아도 체크를 하는 것이다.

원소가 추가될 때마다 2의 배수로 자동으로 메모리를 늘려 재할당하는 모습을 볼 수 있다.

이게 편하긴 하지만 메모리를 재할당 할 때 메모리에 잡힌 모든 원소를 복사해놓고 메모리를 해제시킨 다음 다시 필요한 만큼 재할당 하기 때문에 시간적으로 손해가 많다.

예를 들어서 출력의 1 / 1 아래의 0번 원소의 주소값 끝이 30이지만 재할당 한 직후인 2 / 2 아래의 0번원소의 주소값 끝이 50으로 끝난다.

메모리를 새로 할당한다는 뜻이다.

때문에 이런식으로 할당해서 사용하는 방법은 지양하는 것이 좋겠다.

 

#include <iostream>
#include <vector>

using namespace std;

int main() {
    // int형을 저장하는 vector
    vector<int> v1(3);

    cout << v1.capacity() << '\n';	// 1번
    v1.push_back(1);
    cout << v1.capacity() << '\n';	// 2번
    v1.push_back(2);
    cout << v1.capacity() << '\n';	// 3번
    v1.push_back(3);
    cout << v1.capacity() << '\n';	// 4번
    v1.push_back(4);
    cout << v1.capacity() << '\n';	// 5번
    v1.push_back(5);
    cout << v1.capacity() << '\n';	// 6번
  
    return 0;
}

다음은 두 번째 선언 방법이다. 위처럼 3개 할당하면서 선언을 해 보자.

그러면 capacity()로 출력을 했을 때, 처음 3개 할당되었으니 주석으로 4번까지는 3이 출력되고 5번부터는 6이 출력되어야 할 것이다.

결과는???

왜 다 안찼는데 지맘대로 할당하지?????

 

이유는 이렇다.

1번 출력을 하기 전에 v1을 순차적으로 탐색하며 0번 원소부터 훑어보자.

3개 할당했으니 3개는 값을 볼 수 있다.

이렇게 크기를 정해서 할당하게 되면 값이 0으로 초기화되어서 들어가있다.

때문에 1번출력까지는 추가로 넣은게 없으니 3이 출력되고

2번부터는 1개 더 넣어서 원소 4개를 담아야하니 3 * 2만큼의 capacity가 출력되는 것이다.

 

vector<int> v1(3, 1);
vector<int> v2 = {1, 2, 3};

세 번째, 네번째 방법으로 선언해 초기화를 해도 같은 결과이다.


vector 다루기

원소 참조

vector<int> v1 = { 3, 2, 1 };

cout << v1[0] << '\n';      // 배열처럼 접근 가능
cout << v1.at(1) << '\n';   // 인덱스 지정
cout << v1.front() << '\n'; // 가장 첫 번째 원소
cout << v1.back();          // 가장 마지막 원소

 

원소 추가

vector<int> v1;
vector<int> v2(3);

// push_back()을 통해 가장 뒤쪽에 원소 추가
v1.push_back(5);
v1.push_back(6);
v1.push_back(3);

cout << "v1: ";
for(int i = 0; i < v1.size(); i++) {
    cout << v1[i] << ' ';
}
cout << '\n';

// 인덱스 접근을 통해 해당 인덱스에 원소 추가(배열처럼)
v2[0] = 8;
v2[1] = 7;
v2[2] = 6;

cout << "v2: ";
for(int i = 0; i < v2.size(); i++) {
    cout << v2[i] << ' ';
}
cout << '\n';

** 주의사항: 두 번째의 인덱스 접근을 통해 원소를 추가하는 방법은 메모리를 자동으로 할당하지 않는다.

따라서 접근하려는 인덱스가 벡터가 할당한 인덱스여야지만 사용할 수 있는 방법이며, 속도향상을 위해 자동할당을 피하려면 push_back보다는 미리 할당하고 인덱스로 접근해 해당 방법으로 사용할 수 있다.

 

원소 제거, 기타

    vector<int> v1 = { 1, 2, 3, 4, 5 };
    
    v1.pop_back();
    v1.pop_back();

    cout << "capacity: " << v1.capacity() << '\n';

    cout << "v1: ";
    for(int i = 0; i < v1.size(); i++) {
        cout << v1[i] << ' ';
    }
    cout << '\n';

    cout << "capacity: " << v1.capacity() << '\n';

    v1.clear();

    cout << "v1: ";
    for(int i = 0; i < v1.size(); i++) {
        cout << v1[i] << ' ';
    }
    cout << '\n';

    cout << "capacity: " << v1.capacity() << '\n';

pop_back()을 사용하면 가장 마지막 원소를 삭제한다.

clear()를 사용하면 원소를 전부 제거한다.

참고로 삭제한다고 해서 capacity가 줄어들지는 않는다. 즉, 메모리 재할당을 하지 않는다.

 

    vector<int> v1 = { 1, 2, 3, 4, 5 };

    cout << "capacity: " << v1.capacity() << '\n';
    cout << "size: " << v1.size() << '\n';
    printVector(v1);

    v1.resize(8);	// 리사이즈 8
    cout << "capacity: " << v1.capacity() << '\n';
    cout << "size: " << v1.size() << '\n';
    printVector(v1);

    v1.resize(5);	// 리사이즈 5
    cout << "capacity: " << v1.capacity() << '\n';
    cout << "size: " << v1.size() << '\n';
    printVector(v1);

    v1.resize(1);	// 리사이즈 1
    cout << "capacity: " << v1.capacity() << '\n';
    cout << "size: " << v1.size() << '\n';
    printVector(v1);

    return 0;

resize()를 하면 capacity(총 용량)가 조절되는게 아니라 size(원소 갯수)가 조절된다.

따라서 resize()로 현재 size보다 크게 조절하면 0인 원소들을 더 만들어서 늘리고 capacity가 부족하면 2배씩 해서 capacity도 늘린다.

resize()로 작게 조절한다고 해도 capacity는 줄어들지 않고 원소 갯수만 줄어든다.

 

위에 원소 추가부분에 "속도향상을 위해 자동할당을 피하려면 push_back보다는 미리 할당하고 인덱스로 접근해 해당 방법으로 사용할 수 있다." 라고 했는데 벡터를 선언하고 resize()로 알맞은 크기로 늘린다음 인덱스 접근으로 원소를 집어넣는 방법을 사용할 수 있다.

 

    vector<int> v1 = { 1, 2, 3, 4, 5 };

    cout << "empty: " << v1.empty() << '\n';

    v1.clear();
    
    cout << "empty: " << v1.empty() << '\n';

 

empty()를 사용하면 모든 원소가 비었을 때(즉, size()가 0일 때) 참값을 반환한다.


iterator

iterator는 반복자라고 하며, C++라이브러리가 제공한다.

이걸 사용하면 라이브러리의 방식대로 자료구조에 접근할 수 있으며, 효과적으로 컨테이너에 저장된 원소들을 탐색할 수 있다.

STL편 컨테이너의 설명에서 각 컨테이너는 원소를 저장하고 접근하는 방식이 다 달랐다.

때문에 반복자도 이에 따라 각기 다른 방식으로 컨테이너를 탐색하며, 그렇기에 반복자에도 여러 종류가 있다.

여기서는 vector편이니까 당연히 vector의 반복자를 사용하겠다.

 

반복자는 쉽게 생각하면 포인터와 같다.

벡터로 예를 들건데, 그 전에 벡터에 begin()을 쓰면 원소의 첫 주소를 가리키고(배열 이름처럼),

반복자 선언은 vector<타입>::iterator 이름; 의 형태인 것을 알아야 한다.

int main() {
    vector<int> v1 = { 1, 2, 3, 4 };

    vector<int>::iterator iter = v1.begin();

    cout << v1[0] << '\n';          // 인덱스로 원소 접근
    cout << *(v1.begin()) << '\n';  // 벡터 주소로 접근
    cout << *iter << '\n';          // 반복자로 접근

    return 0;
}

 

위 코드를 실행하면 결과 3개가 첫 원소인 1로 출력되는 것을 볼 수 있다.

 

그리고 첫 원소의 주소를 가리키는 begin()이 있다면 end()도 있다.

여기서 주의할 점은 end()는 마지막 원소의 주소가 아닌 마지막 원소 다음의 주소이며, 그림으로 요약하면 아래와 같다.

또한 반복자는 ++, --, ==, !=와같은 연산이 가능하기에, 반복자와 for문으로 원소를 차례차례 탐색하는 예시를 보자.

int main() {
    int arr[4] = { 1, 2, 3, 4 };
    int *arrEnd = arr + (sizeof(arr) / sizeof(int));
    int *pIter;
    for(pIter = arr; pIter != arrEnd; pIter++) {
        cout << *pIter << ' ';
    }
    cout << '\n';

    vector<int> v1 = { 5, 6, 7, 8 };
    vector<int>::iterator iter;
    for(iter = v1.begin(); iter != v1.end(); iter++) {
        cout << *iter << ' ';
    }

    return 0;
}

굳이 포인터와 비교해서 보자면 위와 같고, 이 예시로 중요하게 기억해야 할 점은 end()함수를 사용하면 마지막 원소 다음의 주소를 가리킨다는 것과, ++연산이 가능하다는 것, iterator로 담은 값은 주소값이기에 원소값 그 자체를 보려면 앞에 *를 붙혀야 한다는 것이다.

 

그렇다면 for문과 반복자로 순차 탐색하는 것은 알겠는데, 코드가 너무 길어요, 좀 더 줄이고싶어요~~~

int main() {
    vector<int> v1 = { 5, 6, 7, 8 };
    for(vector<int>::iterator iter = v1.begin(); iter != v1.end(); iter++) {
        cout << *iter << ' ';
    }

    return 0;
}

이렇게 for문안에 선언을 하는식으로 바꾸면 된다.

????장난하나 그래도 더 길잖아요~~~

 

int main() {
    vector<int> v1 = { 5, 6, 7, 8 };
    for(auto iter = v1.begin(); iter != v1.end(); iter++) {
        cout << *iter << ' ';
    }

    return 0;
}

와 쓸만하게 많이 줄었네요

C++11부터 auto를 사용해 C++가 자료형을 추론할 수 있습니다!

 

int main() {
    vector<int> v1 = { 5, 6, 7, 8 };
    for(int temp : v1) {
        cout << temp << ' ';
    }

    return 0;
}

범위기반 for문(ranged-based for loop)을 사용하면 훨씬 간편하긴 하다.

하지만, 이 방법은 iterator가 아니고, 단순히 첫 원소부터 마지막 원소까지 순차적으로 훑을때만 사용할 수 있으며, 당연하지만 벡터의 원소 자료형과 for문 왼쪽 변수의 자료형이 같아야 하고, auto를 사용하는 방법도 있다.

해당 방법은 vector뿐만 아니라, 고정 길이의 array, map, set, list등 다양한 곳에서 쓰일 수 있다.

728x90

'개발 > C, C++' 카테고리의 다른 글

[C++] 네이밍  (0) 2022.07.08
[C++] STL 1편 - STL이란?  (0) 2022.07.07
[C++] XOR 연산 이용 swap  (0) 2022.06.12
[C++] 참조자(Reference), 포인터와 차이점, 사용법  (0) 2022.06.10
[C++] namespace란?  (0) 2022.06.10
COMMENT