PS에서 사용하는 C++ STL - 초급 (vector)

2021. 8. 20. 23:44알고리즘 문제풀기/코딩

도입

vector("벡터")는 간단하게 말하면 기능이 몇 개 추가된 배열입니다. 수학에서 말하는 벡터, "기하와 벡터" 할 때의 그 벡터를 떠올리는 분도 있겠지요. 관련이 완전히 없는 것은 아니지만 크게 중요하지는 않아요.

사용 방법

코드 첫 줄에 다음을 적습니다.

#include <vector>

벡터는 배열과 비슷하게 변수처럼 선언합니다. 단 타입명을 꺽쇠로 감싸서 명시해 주어요.

아래 코드는 벡터를 쓰지 않은 코드입니다.

#include <iostream>
using namespace std;

int a[100];
int main() {
    cin >> a[0];
    cin >> a[1];
    cin >> a[12];
    cout << (a[1] + a[2] + a[3]) << endl;
    return 0;
}

아래 코드는 벡터를 쓰는 코드입니다. 동작은 동일해요.

#include <iostream>
#include <vector>
using namespace std;

vector<int> a(100);
int main() {
    cin >> a[0];
    cin >> a[1];
    cin >> a[12];
    cout << (a[1] + a[2] + a[3]) << endl;
    return 0;
}

두 코드의 차이가 뭘까요? #include 문이 생겼고, 배열 선언이 vector 선언으로 대체되었어요. 즉 vector<int> a(100);int a[100];와 비슷한 효과를 낸다는 거지요.

<int> 이렇게 꺽쇠로 감싼 부분이 처음 보면 낯설지도 몰라요. 헤더파일 이름을 감싸는 것과 똑같은 모양이죠. 비슷하게 short형이나 char 배열도 만들 수 있어요.

#include <vector>
using namespace std;

int main() {
    vector<short> vA(5);
    vector<char> vB(3);

    vA[0] = 3;
    vA[1]++;
    vA[2] = vA[0] + 5;
    vA[3] = vA[1] * vA[2];

    vB[0] = 'a';
    vB[2] = '\n';

    return 0;
}

잠깐! 여기서 뭔가 이상한 걸 눈치채셨나요? 아래 코드를 읽어 보세요.

// ...
int main() {
    short vA[5];
    char vB[3];

    vA[0] = 3;
    vA[1]++;   // <----- ???
    // ...
}

로컬 변수(함수 내에서 선언한 변수)를 초기화하지 않고 사용하면 처음 값을 보장할 수 없지요. 그러니까 이 배열을 사용한 코드에서 vA[1]의 처음 값은 정해져 있지 않고, ++ 연산을 적용한 후의 값도 당연히 알 수 없어요. 그런데 vector를 이렇게 쓰는 경우엔 일일이 초기화하지 않아도 값이 모두 0임이 보장되어 있어요. 그러니 벡터를 사용한 코드에서 vA[1]++;의 연산을 거치기 전에는 값이 확실히 0이고 이후에는 1이 돼요.

벡터는 초기값도 설정할 수 있고, 함수의 반환형으로도 쓸 수 있어요.

vector<int> f() {
    vector<int> ret(5);
    ret[0] = 3;
    return ret;
}

vector<int> g() {
    vector<int> ret = {1, 2, 3};
    return ret;
}

vector<int> h() {
    return {1, 2, 3};
}
  • 참고로 위의 코드는 C++11 이후의 표준을 준수하는 환경(C++11, C++14, C++17, C++20, ……)에서만 컴파일 가능합니다. 무슨 말인지 모르시겠다면 온라인 저지에서 언어를 선택할 때 C++14 혹은 C++17 등 최신 사양을 선택하시면 돼요.

g()를 보면 마치 배열을 초기화하는 것 같죠? h()는 훨씬 간단해서 편리하구요.

인덱스와 크기

벡터의 인덱스는 0에서 (크기)-1까지만 사용할 수 있습니다. 배열과 마찬가지예요. 즉 int a[5]; 혹은 vector<int> a(5);에 대해 a[0]부터 a[4]까지의 값은 문제 없이 사용 가능하지만 a[-1]이나 a[5], a[9999]같은 값에 접근을 시도해서는 안 돼요.

벡터의 크기도 따로 확인할 수 있어요.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {1, 2, 3};
    cout << v.size() << endl;   /* prints: 3 */
    return 0;
}

다만 여기서 또 주의할 점이 있어요. v.size()의 값은 size_t 타입이고, 이건 환경에 따라 다르지만 보통 unsigned long 타입이에요. long이 32비트인지 64비트인지가 중요한 게 아니라 unsigned라는 사실이 중요해요! 그러니까 이런 일이 발생해요.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {1, 2, 3};
    cout << (v.size()-4) << endl;
    /* prints: 18446744073709551615 */

    cout << (v.size() > -4 ? "v.size() > -4" : "v.size() < -4") << endl;
    /* prints: v.size() < -4 */

    return 0;
}

즉 예상치 못한 곳에서 unsigned underflow가 일어나기 쉽고, 비교나 산술 연산 시에 음수가 unsigned로 변환되어 굉장히 큰 양수로 취급되기 쉬워요. 컴파일러가 경고를 띄워 주기도 하지만, 이런 건 스스로 잡을 수 있어야겠죠?

원소 추가

벡터가 일반 배열에 비해 훨씬 강력한 이유는 바로 원소를 동적으로 추가/삭제할 수 있기 때문이에요. 다음 코드를 볼까요?

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {1, 2, 3};
    cout << v[2] << endl;   // v = {1, 2, 3}; prints 3

    v.push_back(4);         // v = {1, 2, 3, 4}
    cout << v[3] << endl;   // prints 4

    v.pop_back();               // v = {1, 2, 3}
    cout << v.size() << endl;   // prints 3

    v.push_back(-5);        // v = {1, 2, 3, -5}
    cout << v[3] << endl;   // prints -5

    return 0;
}

push_back(x) 함수로 x라는 값의 원소를 제일 뒤에 추가할 수 있고, pop_back() 함수로 제일 뒤의 원소를 제거할 수 있어요. 원소의 추가 및 제거에 따라 size()의 값도 바뀌구요. 이 점이 배열보다 낫죠. 배열을 int a[3]; 처럼 선언하고 나면 a[3]은 영영 접근할 방법이 없는데, 벡터는 크기를 자유자재로 늘릴 수 있어요.

원소 순회

배열의 크기를 알면 배열을 순회할 수 있죠.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {2, 3, 5};
    for (int i = 0; i < (int)v.size(); ++i) {
        cout << i << ": " << v[i] << endl;
    }
    return 0;
}
/* prints:
       0: 2
       1: 3
       2: 5
 */

int 변수로 인덱스를 순회할 때에는 위의 소스처럼 배열의 크기를 int형으로 변환해서 비교해 주는 게 좋아요. 아니면 iunsigned int 형으로 변환해도 되구요.

인덱스를 전혀 안 쓰고 순회하는 방법도 있어요.

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {2, 3, 5};
    for (int x : v) {
        cout << x << endl;
    }
    return 0;
}
/* prints:
       2
       3
       5
 */

for문 안쪽에 보통 쓰던 것처럼 세미콜론(;)이 없고 콜론(:)이 있죠. 자바, 파이썬, 자바스크립트 등 다른 많은 언어가 비슷한 문법을 지원해요.

마치며

여기까지가 벡터의 기초적인 사용 방법이에요. 이것 말고도 기능이 많이 있는데 다른 글에서 살펴 보겠습니다.

1 2 3 4 5 6