본문 바로가기

C++

[C++] 표준 템플릿 라이브러리(STL)(1) std::vector

C++에서 가장 많이 사용하는 헤더 중 하나가 바로 <vector>이다. C에 있는 동적 배열을 캡슐화한 객체이다. 얘 때문에 C보다 array형 data를 다루기 편하다. 또한 push_back, size, clear 같은 멤버 함수와 iterator를 통한 <algorithm>에 선언된 STL 함수에 요긴하게 사용된다.

1. vector 생성(construct), 접근(iterate), 이동(move/assign)

1-1 생성(construct)

std::vector<int> vec1;
std::vector<int> vec2(vec1);
std::vector<int> vec4{1, 2, 3};

기본적으로(using namespace std를 쓰지 않는다는 가정하에) std::vector<원소자료형> 벡터이름 꼴로 벡터 객체를 생성한다.
vec1의 경우, int 형 원소를 갖는 빈 벡터를 생성한다.

vec2의 경우, 복사 생성자(copy constructor)를 호출한다. 이 말은 vec1의 모든 원소와 특성이 vec2에 그대로 복사된다는 의미이다. 복사 생성자(copy constructor)는 추후에 서술할 예정이다.

vec4의 경우, 원소를 1, 2, 3으로 가지는 벡터 객체를 생성한다. 이는 initializer-list를 이용한 선언이다. C++11 이상부터 지원되는 생성자이며 member initializer list와 혼동하지 말아야한다. 추후에 다시 서술할 예정이다.

1-2 접근(iterate)

vec1.begin(), vec1.end();
vec1.cbegin(), vec1.cend();
vec1.rbegin(), vec1.rend();

iterator는 C++에서 unique_ptr, shared_ptr과 같은 포인터의 추상화 객체이다. 다른 점은 포인터가 가리키고 있는 객체의 자료구조형에 상관없이 next 연산(현재 객체에서 같은 컨테이너의 다른 객체로 접근), 이를테면 ++ 연산을 지원한다.

begin(), end()는 각각 벡터의 0번 위치와 마지막 인덱스 + 1 위치를 가리킨다. 왜 이렇게 디자인 됐을까 생각해봤을 때, 함수 argument로 begin()과 end()를 자주 보내는 것과 관련있지 않을까 싶다. 둘이 완전히 같은 종류의 iterator라면, 즉 0번 위치와 마지막 인덱스 위치를 가리킨다면, 함수가 온전히 동작하지 않을 수도 있겠다고 생각이 들었다. 예를 들어, 마지막 인덱스 이후 데이터가 들어가는 경우이다.
cbegin(), cend()는 const가 붙었다고 생각하면 된다. 가리키는 데이터를 조작하지 못한다. 단순 출력 반복문이라면 안전을 위해 cbegin()/cend()를 사용하면 더 robust한 코드가 된다.

rbegin(), rend()는 파이썬의 마이너스 인덱싱이라고 보면 된다. 조금 헷갈릴 수 있는게, rbegin()은 마지막 원소를 가리키고 rend()는 0번 위치 바로 이전 -1번 위치를 가리킨다.

1-3 이동(move/assign)

vec1 = vec2;
vec1 = std::move(vec2);
vec1 = {10, 20, 30};

 

2. vector의 크기

2-1 size() vs .capacity()

벡터에서 동적 할당을 덜 신경써도 되는 이유이다.
쉽게 설명하자면
size() : 벡터에 저장된 데이터(element) 개수
capacity() : 벡터에 할당된 메모리 크기. 즉, 동적 할당과 연관이 깊다.

size와 capacity는 서로 연관될 수 밖에 없는데, 메모리 관리를 해야하는 경우라면 이를 꼼꼼히 알아두어야 한다.

size와 capacity 비교

2-2 크기

vec1.empty();
vec1.size();

1-3 지우기

vec1.clear();

vec1에 있는 모든 원소를 지운다. 하지만 이는 벡터 원소를 실제로 지우지 않는다.

메모리 관점에서, clear()는 벡터가 할당된 영역을 해제(free, delete)하지 않는다. 만약 clear()로 벡터 원소를 다 지우고 새로운 원소를 집어 넣는다면, 이 벡터는 새로 메모리를 할당받지 않는다. 그렇다면 clear()를 어떻게 봐야 할까? 벡터 원소 자체가 소멸(detructed)한다 보면 된다. 실제로 억지로 capacity()를 사용해서 벡터 원소를 확인하면 이상한 값이 나온다. 참고로 할당된 메모리 영역을 해제하려면(freed) shrink_to_fit()으로 clear()후 size와 capacity를 맞춘다.

2-2 .push_back()

std::vector<int> num{3, 2, 5, 4, 7};    // num = {3, 2, 5, 4, 7}
num.push_back(8);                        // num = {3, 2, 5, 4, 7, 8}

※ pop_back()과 같이 사용하면 vector를 stack처럼 사용할 수 있다.

#include <iostream>
#include <vector>

int main(){
    std::vector<int> num{3, 2, 5, 4, 7};
    std::cout << "*******************" <<std::endl; 
    std::cout << "before push_back" <<std::endl; 
    for(auto it: num){
        std::cout << it << ' ';
    }
    std::cout << std::endl << "num size: " << num.size() << std::endl;
    std::cout << "num capacity: " << num.capacity() << std::endl << std::endl;

    num.push_back(8);

    std::cout << "*******************" <<std::endl; 
    std::cout << "after push_back" <<std::endl; 
    for(auto it: num){
        std::cout << it << ' ';
    }
    std::cout << std::endl << "num size: " << num.size() << std::endl;
    std::cout << "num capacity: " << num.capacity() << std::endl;

    std::cout << "*******************" <<std::endl; 

    return 0;
}

num size, num capacity의 변화를 주목해서 살펴보자.

2-3 insert()

다음 초기화된 벡터를 기준으로 보자

std::vector<int> num{3, 2, 5, 4, 7};

.insert()는 2개 혹은 3개의 argument를 넣을 수 있다.

num.insert(num.begin()+3, 1);

argument가 2개인 경우는 특정 위치에 값을 하나 넣을 때 사용한다.
앞 argument에는 원소를 삽입할 위치(iterator로 표현)가 들어간다. 위 예시로는 num.begin()+3 혹은 3번째 index 위치에 들어간다는 뜻이다. 그 뒤에는 삽입할 데이터가 들어간다. 위 예시로는 1이 3번 index로 들어간다는 뜻이다.

#include <iostream>
#include <vector>

int main(){
    std::vector<int> num{3, 2, 5, 4, 7};
    std::cout << "*******************" <<std::endl; 
    std::cout << "before insert" <<std::endl; 
    for(auto it: num){
        std::cout << it << ' ';
    }
    std::cout << std::endl << "num size: " << num.size() << std::endl;
    std::cout << "num capacity: " << num.capacity() << std::endl << std::endl;

    num.insert(num.begin()+3, 1);

    std::cout << "*******************" <<std::endl; 
    std::cout << "after insert" <<std::endl; 
    for(auto it: num){
        std::cout << it << ' ';
    }
    std::cout << std::endl << "num size: " << num.size() << std::endl;
    std::cout << "num capacity: " << num.capacity() << std::endl;

    std::cout << "*******************" <<std::endl; 

    return 0;
}

num size, num capacity의 변화를 주목해서 살펴보자.

num.insert(num.begin()+3, 2, 1);

argument가 3개인 경우는 특정 위치에 같은 값 여러 개를 넣을때 사용한다.
맨 앞에는 원소를 삽입할 위치(iterator로 표현)가 들어간다. 위 예시로는 num.begin()+3 혹은 3번째 index 위치에 들어간다는 뜻이다. 중간에는 삽입할 데이터 개수가 들어간다. 위 예시로는 같은 데이터 2개가 3번 index부터 차례로 들어간다는 뜻이다. 맨 뒤에는 삽입할 데이터가 들어간다. 위 예시로는 1 2개가 3번 index부터 차례로 들어간다는 뜻이다.

#include <iostream>
#include <vector>

int main(){
    std::vector<int> num{3, 2, 5, 4, 7};
    std::cout << "*******************" <<std::endl; 
    std::cout << "before insert" <<std::endl; 
    for(auto it: num){
        std::cout << it << ' ';
    }
    std::cout << std::endl << "num size: " << num.size() << std::endl;
    std::cout << "num capacity: " << num.capacity() << std::endl << std::endl;

    num.insert(num.begin()+3, 2, 1);

    std::cout << "*******************" <<std::endl; 
    std::cout << "after insert" <<std::endl; 
    for(auto it: num){
        std::cout << it << ' ';
    }
    std::cout << std::endl << "num size: " << num.size() << std::endl;
    std::cout << "num capacity: " << num.capacity() << std::endl;

    std::cout << "*******************" <<std::endl; 

    return 0;
}

num size, num capacity의 변화를 주목해서 살펴보자.

2-4 `erase()

erase()는 1개 혹은 2개의 argument를 넣을 수 있다.

num.insert(num.begin()+3);

argument가 1개인 경우는 특정 위치 값을 없앨때 사용한다.
위 예시로는 num.begin()+3 혹은 3번 index 원소를 없앤다는 뜻이다.

#include <iostream>
#include <vector>

int main(){
    std::vector<int> num{3, 2, 5, 4, 7};
    std::cout << "*******************" <<std::endl; 
    std::cout << "before erase" <<std::endl; 
    for(auto it: num){
        std::cout << it << ' ';
    }
    std::cout << std::endl << "num size: " << num.size() << std::endl;
    std::cout << "num capacity: " << num.capacity() << std::endl << std::endl;

    num.erase(num.begin()+3);

    std::cout << "*******************" <<std::endl; 
    std::cout << "after erase" <<std::endl; 
    for(auto it: num){
        std::cout << it << ' ';
    }
    std::cout << std::endl << "num size: " << num.size() << std::endl;
    std::cout << "num capacity: " << num.capacity() << std::endl;

    std::cout << "*******************" <<std::endl; 

    return 0;
}

num size, num capacity의 변화를 주목해서 살펴보자.

num.erase(num.begin()+3, num.begin()+5);

// num.begin()+5 대신 num.end() 가능

argument가 2개인 경우는 없앨 원소의 시작 위치와 끝 위치가 들어간다.
앞에는 원소를 없애기 시작할 위치가 들어간다. 위 예시로는 num.begin()+3 혹은 3번 index부터 없앤다는 뜻이다. 뒤에는 없애는 동작을 멈출 위치가 들어간다. 위 예시로는 num.begin()+5 혹은 5번 index에서 erase 동작을 멈춘다는 뜻이다. 주의해야 할 점이 멈출 위치에서는 erase가 이루어지지 않는다는 뜻이다. 위 예시에서 num.begin()+5는 num.end()와 같은데, num은 4번 index까지 밖에 없다. 마지막 iterator인 num.end() 혹은 num.begin()+5는 벡터 원소를 가르키지 않는다.

#include <iostream>
#include <vector>

int main(){
    std::vector<int> num{3, 2, 5, 4, 7};
    std::cout << "*******************" <<std::endl; 
    std::cout << "before erase" <<std::endl; 
    for(auto it: num){
        std::cout << it << ' ';
    }
    std::cout << std::endl << "num size: " << num.size() << std::endl;
    std::cout << "num capacity: " << num.capacity() << std::endl << std::endl;

    num.erase(num.begin()+3, num.begin()+5);

    std::cout << "*******************" <<std::endl; 
    std::cout << "after erase" <<std::endl; 
    for(auto it: num){
        std::cout << it << ' ';
    }
    std::cout << std::endl << "num size: " << num.size() << std::endl;
    std::cout << "num capacity: " << num.capacity() << std::endl;

    std::cout << "*******************" <<std::endl; 

    return 0;
}

num size, num capacity의 변화를 주목해서 살펴보자.

이렇게까지 길어질 줄 몰랐다. 가장 중요한 vectormap은 각각 포스팅 하나로 정리해야겠다.