티스토리 뷰

C++

[C++ STL] Vector

XXIN-dev 2021. 4. 28. 22:41

> Vector?

메모리 heap에 동적할당되는 자료의 길이를 변경할 수 있는 배열입니다.

 

 


먼저, 메모리 heap과 동적할당에 대해서 설명해보고자 합니다.

 

다들 메모리 안에 구조가 어떻게 생겼는지 아시나요? 저도 heap영역, stack영역 많이 들어봤었지만 깊게 공부해본 적이 없더군요. 그래서 이번 기회를 빌어 조금 깊게 해당 부분에 대해서 공부를 해봤습니다.

 

메모리의 구조

스택 영역함수가 호출되었을 때 호출이 되었다가 함수가 종료됨과 함께 사라지는 변수들을 생각하시면 될 것 같습니다. 따라서 컴파일 과정에서 해당 변수들은 크기가 정해지고 종료되면서 변수들도 없어집니다.

 

이와 반대로 힙 영역은 사용자가 관리합니다. 사용자가 동적으로 변수를 할당하고 해제할 수 있습니다. 

잠깐만 사용했다가 해제하고 싶은 변수, 함수가 종료되고 나서도 해당 변수를 사용하고 싶을 때, 크기를 알 수 없는 배열을 사용하고 싶을 때 힙 영역에 동적으로 변수를 할당하고 사용하면 됩니다.

동적 할당은 하고나서는 꼭 해제를 해줘야지 메모리 누수가 발생하지 않습니다. 

Java의 경우에는 가비지 콜렉터가 알아서 사용되지 않는 메모리를 처리해주지만 C/C++은 사용자가 동적으로 메모리를 할당했다면 알아서 해제시켜줘야 합니다 !!

 


배열은 고정된 크기의 자료구조로 선언해줄 때 크기를 지정하지 않으면 사용이 불가능합니다.

 

이러한 배열의 문제점을 해결하기 위해서는 배열을 동적할당해서 사용해야 했는데요, 그 방법 중에 하나가 Vector로 크기가 가변적인 배열을 만드는 것입니다. 

 

물론 속도 성능은 배열이 Vector보다는 좋기 때문에 크기가 가변적이지만 않는다면 그냥 배열을 쓰는 걸 추천드립니다. 하지만 Vector가 메모리 관리면에서 배열보다는 좋기 때문에 배열의 크기가 가변적이고 얼만큼의 원소가 들어올 지 예상하지 못하겠다면 Vector를 쓰는 것을 추천드립니다.

 

그럼 Vector에 대해서 더 자세하게 살펴보겠습니다.

 

 


> iterator

iterator는 컨테이너 안 원소를 참조하며 포인터와 비슷합니다.

#include <iostream>
#include <vector>
using namespace std;
int main(){
	vector<int> v;
	for(int i=0; i<10; i++){
		v.push_back(i);
	}

	for(auto it = v.begin(); it!=v.end(); it++){
		if(*it % 2 == 0){
			*it = 2;
		}
		cout << *it << " ";
	}
	//출력 : 2 1 2 3 2 5 2 7 2 9
}

begin(), end()는 Vector 안의 원소를 참조해서 쓰기 때문에 해당 원소의 값을 사용하려면 포인터처럼 * 를 붙여줘야 합니다.

begin(), end()과는 반대로 reverse된 상태의 begin과 end를 잡는 rbegin()과 rend()도 있습니다.

 

 

> 접근

#include <iostream>
#include <vector>
using namespace std;
int main(){
	vector<int> v;
	for(int i=0; i<10; i++){
		v.push_back(i);
	}
	
    // 인덱스 1 요소 접근
    cout << v.at(1) << " ";
    // 인덱스 1 요소 접근
    cout << v[1] << " ";
    // 맨 앞 요소 접근
    cout << v.front() << " ";
    // 맨 뒤 요소 접근
    cout << v.back() << endl;
    // 출력 : 1 1 0 9
}

at과 []이 어떤 차이가 있는지 궁금하지 않나요?

at은 범위를 검사하기 때문에 범위 밖에 있는 인덱스를 넣었을 시에 out_of_range 에러가 발생합니다. 

하지만 []는 범위 검사를 하지 않기 때문에 범위 밖에 인덱스를 넣어도 에러가 발생하지 않습니다. 그렇기 때문에 신중하게 사용해야 합니다.

 

 

> 삽입

 

#include <iostream>
#include <vector>
using namespace std;
int main(){
	vector<int> v = {1,2,3,4,5};
	
    	// 맨 뒤에 해당 원소를 넣음
	v.push_back(6);  // [1,2,3,4,5,6]
    	// 맨 뒤에 해당 원소를 넣음
	v.emplace_back(7);  // [1,2,3,4,5,6,7]
  	// 원하는 위치에 해당 원소를 넣음
	v.insert(2, 10); // [1,2,10,3,4,5,6,7]
	// 맨 뒤에 원소 빼기
    	v.pop_back(); // [1,2,10,3,4,5,6]
    	// 원하는 위치에 원소 넣기
    	v.emplace(3, 9); // [1,2,10,9,3,4,5,6]
    	// 원하는 위치의 원소 삭제
    	v.erase(2); // [1,2,9,3,4,5,6]
	//배열 비우기
	v.clear(); // []
}

push_back()과 emplace_back()은 무슨 차이인가 싶겠지만, 돌아가는 방식이 다르다고 보시면 됩니다.

push_back()복사 생성자가 호출되기 때문에 원소를 Vector에 넣을 때마다 복사 생성자가 호출됩니다. 따라서 암시적인 생성자만 호출이 가능합니다. 만약 Vector<구조체> 이런 경우라고 했을 때, push_back()에 해당 구조체를 넣기 위해서는 구조체를 따로 선언해서 push_back()에 넣어줘야 합니다. 

하지만, emplace_back()내부적으로 생성자를 호출하기 때문에 구조체를 따로 선언하지 않고 그대로 emplace_back()안에 선언해줄 수 있습니다. 한마디로 emplace_back()은 다중 parameter를 사용할 때 아주 편합니다. 하지만 에러를 만들어야 할 부분에서 에러를 내지 않는 경우가 많은 함수이니만큼 사용할 때 충분한 이해가 필요할 것 같습니다.

 

삽입에 사용되는 insert의 경우에는 모든 값을 새로운 메모리에 복사한 후에 해당 자리에 값을 넣습니다. 

따라서 overhead로 성능저하를 야기할 수 있습니다.

 

또한, insert와 erase 모두 해당 위치에 원소를 넣을 때, 다른 원소 데이터를 하나하나 이동시킵니다. 이 방식은 Vector의 크기를 재할당하는 문제를 야기할 수 있기 때문에 삽입, 삭제가 빈번하다면 list나 deque를 사용하는 게 더 좋을 것 같습니다.

 

clear()의 경우에는 clear()했을 시에 안에 벡터 요소들만 사라지고 할당된 크기는 그대로 남아있습니다. 이는 공간 비효율로 이어질 수 있기 때문에 swap()이라는 함수를 사용해서 완벽하게 공간을 해제해주어야 합니다. 하지만 이 경우는 한 개의 함수 안에서 재활용되는 경우에만 사용하시길 바랍니다. 함수가 바로 종료되는 경우에는 어차피 힙에서 메모리 해제가 자동으로 되기 때문에 굳이 해주지 않으셔도 됩니다.

 

 

> Capacity

#include <iostream>
#include <vector>
using namespace std;
int main(){
	vector<int> v = {1, 2};
	
    // 벡터가 비었는지 아닌지 true, false로 나타냄
    if(v.empty()) {
    	cout << "빈 배열 true" << endl;
    } else {
    	cout << "안에 원소가 있습니다. false" << endl;
    } // 출력 : 안에 원소가 있습니다. false
    
    // 벡터에 들어갈 수 있는 최대값 리턴
    v.max_size() 
    // 벡터의 현재 사이즈
    v.size() // 출력: 2
    // 벡터에 할당해 준 사이즈
    v.capacity() // 출력 : 사용자가 할당해준 사이즈마다 다르게 나타납니다.
    // 벡터 크기 변경
    v.reserve(6) // == v.resize(6) 
    v.capacity() // 출력: 6
    // 늘어난 버퍼를 줄이고 싶을 경우 사용
    v.resize(2)
    v.shrink_to_fit() 
    v.capacity() // 출력: 2
}

Vector의 capacity 이상으로 원소가 채워진다면 capacity가 재할당됩니다.

하지만 재할당되는 것은 비효율적이기에 그전에 reserve나 resize를 통해서 capacity를 가변적으로 늘릴 수 있습니다.

또한, 큰 capacity에 비해서 적은 원소들이 들어왔다면 size를 줄이고 swap나 shrink_to_fit()를 통해서 버퍼를 줄일 수 있습니다.

 

 

 


많이 쓰이는 스택, 큐에 대해서도 세세하게 알아보고 글을 작성해보도록 하겠습니다.

'C++' 카테고리의 다른 글

[C++] 알고리즘, 시작 전에 자료구조  (1) 2021.04.28
링크
최근에 올라온 글
최근에 달린 댓글