Search

vector의 초기화 (Feat. string)

Created
2021/02/26
tag
C++
vector
string
push_back
cin
reserve
resize

1. push_back vs cin

vector에 요소를 넣을 때는 2가지 방법이 있다.
push_back
cin
두 방법을 이용하여 입력을 받아 요소를 넣어보자.

1) 입력에 대한 테스트

push_back
#include <iostream> #include <vector> std::vector<int> test; int i; int temp; int main(void) { i = -1; while (++i < 5) { std::cin >> temp; test.push_back(temp); } return (0); }
C++
cin
#include <iostream> #include <vector> std::vector<int> test; int i; int main(void) { i = -1; while (++i < 5) std::cin >> test[i]; return (0); }
C++
테스트를 해보면 cin을 이용했을 때 Segmentation Fault를 볼 수 있다. push_backcin에는 어떤 차이가 있을까? 그리고 많은 예제를 확인해보면 cin을 이용해서 입력을 받는 경우도 많은데, 어떻게 해야 cin을 이용하여 입력을 받을 수 있을까?

2) 테스트 결과에 대한 고찰

push_back과 cin의 비교

우선 결론을 내기 전에 몇 가지를 짚어보자.
vector는 클래스이다.
클래스 내부에 vector에 대한 멤버 함수들이 다수 있다.
클래스 내부에는 값들을 홀딩할 수 있도록 멤버 변수를 유지하고 있다.
이를 통해 push_backvector 클래스 내부에 데이터를 홀딩할 뿐 아니라 만일 공간이 모자라 유효한 공간에 데이터를 홀딩 시킬 수 없는 경우에는 유효한 공간을 만들어 내어 데이터를 홀딩 시킬 수 있게 해준다. 반면 cin의 경우는 vector 클래스 내부의 공간을 유효한 공간을 만들어내는 것은 아니지만 그대로 메모리에 쓰는 작업만 수행한다.

Segmentation Fault와 초기화

cin>> 연산자는 참조자를 인자로 받기 때문에 인자로 받은 공간이 유효하지 않은 경우에는 애초에 메모리 접근 자체가 불가능하여 Segmentation Fault를 내도록 되어 있다. vector 클래스 내부 공간이 초기화 되지 않아 유효한 공간을 갖고 있지 않거나, 초기화 된 공간 이상을 접근하여 쓰려고 하면 유효한 공간이 아니기 때문에 Segmentation Fault를 내는 것이 맞다.
따라서 cin을 성공적으로 사용하기 위해선 vector의 초기화를 통해 유효한 공간을 만들어내는 것이 중요하다. 그렇다면 push_back은 유효한 공간이 모자란 경우, 유효한 공간도 창출해주는 것으로 알고 있는데 이 경우에도 vector의 초기화가 중요할까?
push_back으로 데이터를 홀딩시키는 과정 중에 유효한 공간이 모자라 유효한 공간을 창출하는 경우, 공간을 하나를 할당 받아 기존 공간에 갖다 붙이는 것이 아니다. 기존 공간의 2배의 공간을 새로 할당 받고, 기존 공간에 있던 값을 복사하고 기존 공간을 해제하는 방식이다. 따라서 사용하려는 공간의 수를 명확히 하여 사용할 수 있다면 재할당 받는 시간을 아낄 수 있으므로 초기화는 굉장히 중요하다.
공간 할당 혹은 예약생성자 함수, resize, reserve를 통해서 수행할 수 있다. 이에 대해서 알아보자.

2. reserve vs resize

push_back
#include <iostream> #include <vector> std::vector<int> test; int i; int temp; int main(void) { i = -1; while (++i < 5) { std::cin >> temp; test.push_back(temp); } std::sort(test.begin(), test.end()); i = -1; while (++i < 5) std::cout << test[i]; return (0); }
C++
cin
#include <iostream> #include <vector> std::vector<int> test; int i; int main(void) { i = -1; while (++i < 5) std::cin >> test[i]; std::sort(test.begin(), test.end()); i = -1; while (++i < 5) std::cout << test[i]; return (0); }
C++
위와 같은 코드가 있을 때, 반복문을 수행하기 전에 resizereserve를 각각 호출하여 결과를 확인해보자.

1) reserve

vectorreserve 함수는 공간 할당 함수는 아니지만, 공간 예약 함수이다.

push_back

1.
출력
2.
정렬

cin

1.
출력
2.
정렬

정리

reservevector 클래스 내의 데이터를 담기 위해 유효한 공간을 만들지는 않지만, 주어진 크기만큼의 공간을 사용할 것이라고 컴파일러에게 미리 알려주는 역할을 수행한다.
따라서 push_back의 경우에는 주어진 크기까지는 재할당을 받지 않고 push_back의 인자를 vector 클래스에 유효한 공간으로 두어 데이터를 홀딩시킨다.
반면 cin의 경우에는 예약된 공간이기 때문에 주어진 크기까지는 메모리에 값을 쓸 수 있도록 되어 있어 Segmentation Fault는 피할 수 있지만, cin은 단순히 메모리에 값을 쓰는 역할만 수행하므로 vector 클래스 내에 데이터를 홀딩시킬 수 없다.
이 때문에 reserve 함수에 대해선 push_back은 정렬까지 원활히 이뤄졌지만, cin을 이용하면 정렬은 되지 않았던 것이다.
초기 데이터를 홀딩 시킬 때, reserve함수는 push_back과 함께 사용하면 좋다는 것을 유추할 수 있다.

2) resize

vectorresize함수는 공간 할당 함수이다.

push_back

1.
출력
2.
정렬

cin

1.
출력
2.
정렬

정리

resize는 공간 할당 함수이므로 주어진 인자만큼을 유효한 공간으로 두고 특정 값을 vector의 데이터로 홀딩시킨다. (특정 값은 기본적으로 0을 의미한다.)
따라서 push_back을 이용하면 주어진 공간 이상으로 데이터를 홀딩시키려 하기 때문에 초기 크기의 2배만큼을 할당하여 vector의 크기를 재조정한다. 공간이 재조정 된 후에는 push_back의 인자로 들어간 값을 vector 내에 홀딩시킨다
반면 cin의 경우에는 resize의 인자만큼 할당된 공간에 값을 쓰기 때문에, 위의 push_back 경우처럼 공간을 재할당을 받는 일은 없이 홀딩하고 있는 데이터의 값만 바꾸게 된다.
이 때문에 resize 함수에 대해선 push_back은 출력 및 정렬이 원활히 이뤄지지 않았고, cin을 이용했을 때는 출력 및 정렬이 잘 되었던 것을 알 수 있다.
초기 데이터를 홀딩 시킬 때, resize함수는 cin 입력을 받는 것과 함께 사용할 때 좋다는 것을 유추할 수 있다.

3) 명시한 크기 이상으로 push_back을 수행

위에서 코드를 설명하면서, vector 클래스 내부에 주어진 크기 이상으로 push_back을 수행하게 되면 기존 공간의 2배만큼의 공간을 동적으로 할당을 하고 (vector는 동적 컨테이너이다.) 기존 데이터를 새로 할당 받은 공간에 복사한 뒤에 기존 공간을 해제하게 된다고 했다. 아래의 코드로 확인해보자.
#include <iostream> #include <vector> std::vector<int> test; int i; int temp; int main(void) { test = std::vector<int>(5, 0); std::cout << "Capacity: " << test.capacity() << std::endl; std::cout << "Size: " << test.size() << std::endl; i = -1; while (++i < 10) { test.push_back(0); std::cout << "Capacity: " << test.capacity() << std::endl; std::cout << test.size() << std::endl; } return (0); }
C++
실행 결과를 보면 실제로 공간을 2배씩 할당하는 것을 볼 수 있다. 따라서 반복 하여 새로운 값을 push_back을 해야하는 경우 이용할 크기를 정해두지 않으면 위와 같이 매 Capacity를 초과할 때마다 새로운 공간을 할당받고 해제하는 과정을 반복하게 되어 비효율적이다.
위와 같이 반복문을 이용하여 push_back을 수행하는 이유는 컴파일러의 최적화 때문이다. 컴파일러 최적화 덕에 한 번의 push_back을 수행 했을 때는 2배의 공간할당을 받는 것이 아닌 단일 값 1개만큼의 공간을 늘리기 때문이다. 실제로 whileLoop를 없애고 실행해보면 Capacity2배10이 아닌 6이 되는 것을 확인할 수 있다.

3. string

vector에 대해서 resize를 했을 때 내부의 크기도 늘리고 그 원소를 0으로 초기화를 했었다. 반면에 reserve를 진행했을 땐 유효한 공간임만 알리고 내부 크기나 원소 추가는 하지 않았었다.
string은 문자열을 위한 자료구조인데, 생각보다 복잡한 구조로 상속을 받고 있는 객체이며 string_view, utf-8, utf-16, utf-32 등 종류가 다양하다. string은 위에서 언급한 vector와 비슷한 특성이 많다.
stringresize가 있는데, stringresizevectorresize와 동일한 기능을 수행한다. 내부의 크기도 늘리고, string의 원소들에게 '\0'을 갖게 한다. 또한 stringreserve도 갖고 있을 뿐 아니라, append와 비슷한 역할을 수행하는 push_back, pop_back 등도 갖고 있다.
전반적으로 vector의 사용법과 비슷하기 때문에 이를 유념하여 사용하면 된다. (string 역시 vector처럼 공간이 모자라면 2배씩 할당을 받는다.)
string에 대한 멤버 함수가 궁금하면 아래 블로그를 참고하자.