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등 다양한 곳에서 쓰일 수 있다.
'개발 > 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 |