IT Language 연습실
STL Container (2) 'vector' 본문
STL Container 중에서 고정된 크기를 갖는 배열 array에 대해서 알아봤었다.
이번에는 고정된 크기가 아닌 가변 크기를 갖는 배열 vector에 대해서 알아보자.
https://learn.microsoft.com/ko-kr/cpp/standard-library/vector-class?view=msvc-170
vector 클래스
클래스 벡터의 Microsoft C++ 표준 라이브러리 구현에 대한 참조입니다.
learn.microsoft.com
위 사이트를 많이 참고하여 공부했다. 여기서 소개하는 vector에 부족한 부분에 대해서, 혹은 이해가 가지 않는 부분에 대해서는 위 사이트에 들어가 채우거나 보다 더 자세하게 나와 있는 다른 블로그를 방문하여 채우도록 하자.
자 그럼 소개하겠다.
제일 먼저 눈에 보이는 것은 vector 라는 것 역시 std안에 있는 클래스 템플릿을 정의되어 있다.
따라서 이를 사용하기 위해서는 std:: 를 꼭 명시해줘야 하며 컴파일러가 생성할 템플릿 클래스. 자료형을 명시해야 한다.
예시 ) vector < int > OR vector <char> OR vector <pair<int, int>> OR vector <string> 등등
위 예시에서 보이듯 array와 두드러지게 차이가 나는 부분은 바로 사이즈를 명시해주지 않는 것이다.
template <class Type, class Allocator = allocator<Type>>
class vector
그 이유가 allocator 라는 메모리 할당 함수가 받고 있기 때문이다.
즉, allocator 라는 함수를 통해서 메모리를 할당 받고 있다는 말인데,
예를 들면 어느 때는
{ 1, 2, 3, 4, 5 } 라는 요소값을 vector 배열에 넣는다고 그러면 요소 값이 저장되기 위해서는 메모리를
int (정수형 ) 4byte 만큼의 다섯 공간이 필요하다는 말이된다. 즉 4*5 20byte 크기 만큼의 메모리를 할당받아야 한다는 것.
또 어느 때는 3 와 4 사이에 있는 값으로 6 이라는 값을 넣고 싶다면
요소값이 하나가 더 추가 되는 것이니 메모리를 4byte 만큼 더 할당받아야 하며 4와 5의 값이 오른쪽으로 한 칸씩 이동한다
이렇게 가변적으로 크기를 줄였다가 커졌다가 할 수 있어야 한다는 얘기이고 이러한 메모리 크기를
allocator에 의해서 메모리를 받고 있다는 것이다.
---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
자 그럼 다음으로 vector 내에서 사용할 수 있는 함수들을 소개하겠다.
vector 를 사용하는데 있어서 array 와 크게 다르지 않다.
자주 사용하는 함수들을 나열해보겠다.
1) size (저장된 요소값에 대한 사이즈 출력)
2) capacity (현재 할당된 메모리 용량 출력) ,
3) begin() ( 현재 시작 요소 )
4) end() ( 마지막 요소의 다음번지 )
5) reserve (메모리 용량 변경)
6) data (첫번째 번지의 주소 값을 반환)
7) resize (원소의 개수들의 사이즈 값 변경)
8) erase() (요소 삭제)
9) front(첫번째 요소 접근)
10) at(원하는 요소 값 출력 )
11) back (마지막 원소 접근)
12) insert(지정된 번지에 값을 넣어라 번지, 값 )
13) push_back ( ) (마지막 요소에 값 넣기)
14) emplace_back( ) (마지막 요소에 값 넣기)
보면 비슷하게 보이는 함수와 기능적으로 보면 같은 함수들이 존재하는 것을 보이고 있다.
이들의 함수가 어떻게 쓰이고 어떻게 다른지에 대해서 vector를 사용하면서 소개하도록 하겠다.
우선 vector 객체를 생성하고 값을 초기화 할때의 방법이 여러가지가 있다.
1) vector<int> array { 1 } or vector<int> array = { 1 }
2) vector<int> array ( 1 )
3) vector<int> array ( 3, 1 )
위 세가지의 방법의 차이를 알겠는가?
1번의 경우는 배열을 선언하고 배열의 값을 초기화할때 사용하는 방법이라 익숙하다.
2번의 경우는 객체의 생성자를 호출하고 있다는 사실을 확인할 수 있지만 배열 요소에 1을 넣으란 것일까? 의문이다.
왜냐하면 3번의 경우기 때문이다 생성자를 호출해서 배열의 요소값을 넣는 거라면 어째서 3번에서 인자를 두개를 받지?
객체를 생성하고 배열을 선언하고 초기화는 방법은 여러가지지만 저 세개의 경우에서도 조금씩 차이를 보이고 있다.
1번의 경우는 메모리를 요소의 개수 배열에 저장과 동시에 메모리를 사이즈를 할당받는다.
2번의 경우는 생성자 호출을 통하여 메모리 할당을 받는다는 뜻으로 정수를 담을 수 있는 4byte 만큼 메모리를 할당한 것으로 예를들어 array (4) 라면 배열의 크기가 4가 된다는 얘기이다.
3번의 경우는 배열의 크기를 3으로 메모리 할당을 받고 각 배열의 요소 값으로 1을 넣겠다는 말로
array [0] = 1 ; array [1] = 1 ; array[2] = 1 ; 을 했을때의 결과가 동일한 결과를 나타낸다.
그렇기 때문에 1번의 경우로 출력을 한다면 메모리 사이즈는 1과 출력되는 요소는 1하나만 나올 것이고
2번의 경우로 출력을 한다면 메모리 사이즈는 1 요소 값은 없다.
3번의 경우로 출력을 한다면 메모리 사이즈는 3이고 각 요소 값으로 1이 나올 것이다.
이렇게 출력을 해서 알 수 있지만 메모리 크기와 저장된 요소의 개수를 통해서도 알 수 있다.
할당 받은 메모리 크기 ( 배열의 크기 ) 를 알 수 있는 방법, 요소의 개수를 알 수 있는 방법은
1) size (저장된 요소값에 대한 사이즈 출력)
2) capacity (현재 할당된 메모리 용량 출력)
이라는 함수를 사용하면 된다.
만약 array { 1, 2, 3, 4, 5 } 라고 선언을 했다고 치자. 그럼
array.size() 와 array.capacity() 를 사용해주게 됐을때 요소의 개수와 메모리 사이즈를 확인할 수 있다.
그런데 쓰다보면 궁금한점이 다음과 같을 것이다.
size () 와 capacity() 차이가 무엇인지
요소의 개수가 추가됨에 따라 배열의 크기가 결정될 거고 그럼 그럴때마다 size 와 capacity 의 값이 같아지는 것 아닌가?
하지만 요소 개수가 늘어나고 그러다보면 어느순간 size 와 capacity의 결과가 다르게 나오는 것을 확인할 수 있다.
또 이를 reserve 함수를 통하여 메모리 크기를 할당 받는다고 했을때 size와 결과가 다르게 나오는 것을 확인할 수 있다.
예시) array.reserve(10) 이라고 한다면 배열의 크기는 10이 되고 요소의 size()는 5가 된다.
그런데 요소 개수가 계속 늘어나고 그러다보면 어느순간 size 와 capacity의 결과가 다르게 나오는 것을 확인할 수 있다.
라는 말을 했는데 어째서 이러한 결과가 나올까?
이유는 다음과 같다.
표준라이브러리에 정의된 vector를 사용할때 처음에는 배열의 크기를 하나씩 늘려가면서 메모리를 재할당하는 방식으로 사용하고 있었다. 하지만 배열의 크기의 어느 몇퍼센트 남지 않았을때 요소개수를 저장하여 사용하고 있는 경우
또 그러한 경우가 반복되는 경우 하나씩 늘리지 말고 현재 배열의 크기에서 두배정도 되는 크기를 재할당 받는 것이다.
그렇기 때문에 size 와 capacity 의 크기가 달라지는 결과가 보이는 것이다.
그럼 배열의 값을 저장하기 위해서
1) vector<int> array { 1 } or vector<int> array = { 1 }
2) vector<int> array ( 3, 1 )
다음과 같이 사용할 수 있음을 알았다.
하지만 함수를 써서 값을 추가 할 수도 있다.
예를 들어 vector<int> array ; 라고 선언이 됐다면
array.push_back(3);
array.push_back(4);
또는
array.emplace_back(3);
array.emplace_back(4);
즉, push와 emplace 를 통해서 배열에 요소 값을 저장할 수 있게 된다.
그렇다면 둘의 차이는 무엇일까?
1) push_back ( T & ref )
2) push_back ( T && ref )
push_back 은 위 두 코드에서 보이는 것처럼 하나는 복사를 하고 하나는 move 하고 있다.
무슨 소리인지 이해가 가질 않는가? 더 자세히 설명해보겠다.
push_back( ) 함수에 인자가 L value 의 경우 임시객체를 복사생성하고 그렇게 생성된 임시객체를 배열에 저장하겠다 라는 말이 된다. 생성자와 소멸자의 호출이 두번 일어난다는 것을 알 수 있으며 상당히 비효율적이다.
하지만 C++ 11 이상의 환경을 사용중일 경우 인자가 R value의 경우라면 임시객체를 생성후 복사할 필요 없이 값을 넣겠다. 이러한 경우 move를 이용했을때와 동일하다 move( ) L value를 R value로 변환하여 값을 저장하겠다라는 것이니.
그럼 emplace_back (emplace) 는 무엇일까?
이 역시 L value를 받는 경우 R value 를 받는 경우 둘 다 존재하지만 가장 큰 차이점은
해당 컨테이너 내에서 자체적으로 생성이 되고 생성된 것을 배열에 저장하겠다는 것임으로 push 보다는 효율적임을 알 수 있다. 하지만 각각의 장.단점이 있다.
무조건 emplace가 좋다는 것도 아니고 push가 나쁘다는 말도 아니다 필요에 따라 사용하는 것이 제일 올바르다.
---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
위에서 설명되지 않은 함수들의 쓰임을 추가적으로 설명함에 앞서 .
우선 저장된 배열 요소를 출력하는 방법에 대해서 설명하겠다.
vector <int> array { 1, 2, 3, 4, 5 } 라고 저장이 되어 있다고 치자 해당 배열의 요소 값을 추출하는 방법은 다음과 같다.
1) std::cout << array[ 0 ] << ' ' << array[1] << ' ' << array[2] << endl;
2) for (int i = 0; i < array.size(); i++) {
std::cout<< array[i] << ' ';
}
3) for (auto & i : array) {
std::cout<< i << ' ';
}
하지만 함수를 이용한 호출 역시 가능하다.
1) std::cout<< array.front();
2) std::cout<< array.back();
3) for (int i = 0; i < array.size(); i ++ ) {
std::cout<< at(i);
}
4) int * ptr = array.data();
for (int i=0; i<array.size(); i++) {
cout<< *ptr++ << ' ';
}
1번의 경우 array 배열의 [ 0 ] 에 해당하는 요소값을 출력한다.
2번의 경우 array 배열의 [ size() -1 ] 에 해당하는 요소 값을 출력한다.
3번의 경우 array 배열의 각 번지에 해당하는 요소값을 출력한다는 것인데.
4번의 경우 array 배열의 첫번째 주소를 포인터 변수에 저장한 뒤 주소가 가리키는 곳에 접근하여 출력하는 것.
이중에서 3번의 경우 배열의 사이즈를 벗어난 경우 at ( ) 의 요소를 출력하려는 경우 예외 처리가 가능하다는 점.
즉 , at 함수내에서 던져지는 throw는 out_of_range 의 개체이다. 그 개체를 catch에서 받고
what() 어떤 이유로 종료됐는지 문자열로 찍어라 라는 함수.
---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
begin ( ) 과 end ( ) 에 대한 함수는 사실 할 말이 많다.
이곳에서 대략적인 설명을 좀 부여하자면
begin ( ) = 첫번째 요소 번지의 주소를 반환한다고는 한다.
하지만 사실 begin ( )을 들여다보면 iterator의 임시객체 로 반환하고 있다.
iterator 란 무엇일까?
iterator 는 반복자를 말한다. 대부분의 표준라이브러리에 정의된 STL 의 컨테이너 들 말고도 다른 라이브러리에 정의된 녀석들에도 이너 클래스 ( 내부 클래스 )로 존재하는 클래스다.
따라서 배열에 직접 접근을 위한 인덱스 값을 가지고 있을 뿐더러 그 주소가 가리키는 주소가 실질적으로
배열을 만든 해당 클래스에서 갖고 있는 포인터 변수가 가리키는 주소와 동일한지 여부도 확인할 수 있다.
vector<int>::iterator iter = array.begin();
cout << *iter;
그렇기 때문에 위 코드를 보면 배열에 직접 접근을 위한 인덱스 값을 가지고 있다라는 말을 증명한다.
begin( ) 을 통해 반환된 임시객체를 iter 라는 객체가 복사생성자를 호출하여 실질적으로 임시객체를 받고
iter라는 객체가 포인터 비슷한 느낌으로 동작하고 있다.
iter 라는 객체는 정말 포인터가 아니라 iterator 클래스의 한 객체일 뿐이다.
그런데 * 연산을 통해 iter 가 주소를 받고 그 주소가 가리키는 요소 값을 출력하고 있다
이것이 가능한 이유는 * 에 대한 연산자 오버로딩이 정의되어 있기 때문이다.
또한 iterator 에 정의된 연산자 오버로딩은 ++ 전위, 후위 증감 연산과 * 연산 같은 것들도 operator 로 정의가 되어 있다.
배열을 만든 해당 클래스에서 갖고 있는 포인터 변수가 가리키는 주소와 동일한지 여부도 확인할 수 있다 라는 말은
무슨 의미일까?
vector<int> box;
box.push_back(5);
box.emplace_back(10);
vector<int>::iterator iter = box.begin();
cout << *iter << endl;
box.push_back(6);
cout << *iter;
이처럼 box 라는 vector 의 객체를 하나 생성하고 배열 안에 요소 값들을 넣어 놨다.
하지만 capacity 사이즈가 2일때 첫번째 요소의 주소를 iter 가 받았고.
출력했을때 올바르게 출력하고 있음을 알 수 있다.
하지만 그 이후에 다시 box.push_back 을 통한 ( 6 ) 이라는 값을 배열 요소 안으로 저장하려고 할때
다시 메모리를 재할당 받은뒤 capacity를 늘려주고 요소값이 저장되는 흐름을 갖고 있다.
그렇다. 메모리를 재할당.
재할당이 문제가 된다.
iter는 begin () 첫 요소의 주소를 갖고 있었는데 메모리를 재할당 받으면서 begin으로부터 받은 것은 이전의 데이터 요소의 주소를 가리키고 있었고 그 주소는 delete 혹은 free 되면서 사라진 주소가 된다.
그래서 다시 iter 출력해보면 쓰레기 값이 나오는 것을 알 수 있다.
현재 가리키고 있는 주소가 아니라 이전의 주소라는 것이다.
그게 배열을 만든 해당 클래스에서 갖고 있는 포인터 변수가 가리키는 주소와 동일한지 여부도 확인할 수 있다라는 말이
위의 상황을 증명하고 있음을 의미한다.
end ( ) 함수는 똑같이 iterator의 임시객체로 반환을 한다 하지만 마지막 요소 + 1 의 주소를 반환한다.
erase() (요소 삭제) 할때 erase( ) 함수 안에 들어갈 인자는 iterator 타입의 객체이다.
-------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- --------
7) resize (원소의 개수들의 사이즈 값 변경)
원소를 저장할 수 있는 사이즈가 5로 { 1, 2, 3, 4, 5 } 의 값이 저장되어 있다면
array.resize( 7 ) 로 늘리면 사이즈가 { 1, 2,3, 4, 5, 0 ,0 } 이 된다.
12) insert(지정된 번지에 값을 넣어라 번지, 값 )
vector<int> box {1,2,3,4,5};
box.insert(box.begin()+1, 3);
cout << box[1];
{ 1, 3, 3, 4, 5} 가 됐음을 알 수 있다.
-------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- --------
이것이 vector의 사용이다.
하지만 vector의 경우 가변적으로 사용할 수 있음이 너무나도 좋지만 문제는 속도이다.
1, 2, 3, 4, 5 라는 데이터가 저장되어 있는데 3이라는 데이터를 지운다면
4와 5라는 데이터를 왼쪽으로 한 칸씩 옮겨야 하며
첫번째 데이터 7이라는 값을 넣었다면 1, 2, 3, 4 ,5 라는 값이 오른쪽으로 한 칸씩 옮겨져야 한다는 것과 메모리를 다시 그에 맞게 할당 받아야 한다는 불편함이 있다.
과연 이렇게 사용하는 vector 가 속도 부분에서도 좋은 성능을 발휘할지는 잘 모르겠다..
'자료구조 > Array' 카테고리의 다른 글
STL Container (1) 'array' (0) | 2024.02.27 |
---|