본문 바로가기

Program Language and Algorithm

Std::Vector 사용하기

반응형

C++의 기초 문법이라는 험난한 산들을 넘고 나면, Standard Template Library (STL)이라는 산이 기다립니다. 그동안 넘어온 산들에 비하면 진입 장벽이 높지 않으나 잘못 사용하면 독이 되기도 하는 양날의 검이기도 합니다.

 

이 포스팅에서는 STL 중 가장 쉽고, 널리 사용되는 Vector에 대해서 포스팅 하겠습니다.

 

먼저 사용법을 언급하고, 장점을 보여 드린 뒤, 단점을 보여드리려고 했는데, 예제를 못 만들겠어요.. 컴파일러 개발자들 일 잘하네.. 주의점을 보여드리겠습니다.

 

Vector 개념적으 표현하면 동적배열 입니다. 배열을 쓰는데 크기가 동적으로 변화 하며, 사용자가 원하는 대로 늘려서 쓸수 있습니다. 장점은 귀찮기만 메모리 관리를 컴파일러 혹은 OS 알아서 해준다는 거죠. 그동안 포인터에 동적 할당을 해주느라 고생 하셨죠? 이제 vector를 이용하면 훨씬 쉽게 사용하실 수 있습니다. vector의 기본 문법은 아래와 같습니다. 이 문법은 다른 STL 템플릿에도 거의 동일 하니 익숙해지는 게 좋습니다.

#include <vector>
#include <iostream>

using namespace std;

int main(int argc, char ** argv){
// 기본 생성자 5개의 원소를 가지고 각 원소는 0으로 설정.
	vector<int> test(5, 0);

// 배열(포인터)를 사용할 때와 같은 방식으로 이용 가능.
	cout << "Case # 1" << endl;
	for(int i = 0 ; test.size() > i ; i++){
		cout << "test[" << i << "] = " << test[i] << "    ";
	}
	cout << endl;

// push_back 함수 : vector의 끝에 원소를 추가.
	test.push_back(1);

	cout << "Case # 2" << endl;
// iterator 이용. iterator는 템플릿을 순회 하면서 작업할 때 이용 가능. 상황에 따라 [i]와 같은 접근 방식보다 유용할 때가 있음. 
	for(vector<int>::iterator it = test.begin() ; test.end() != it ; it++){
		cout << "test[" << it - test.begin() << "] = " << *it << "    ";
	}
	cout << endl;

// pop_back 함수 : vector의 끝에 있는 원소를 제거.
	test.pop_back();

	cout << "Case # 3" << endl;
	for(vector<int>::iterator it = test.begin() ; test.end() != it ; it++){
		cout << "test[" << it - test.begin() << "] = " << *it << "    ";
	}
	cout << endl;

// insert 함수 : vector의 중간에 값을 추가 할때 사용. 첫 인수는 iterator 형태여야 함. 
	test.insert(test.begin()+ 2 , 2);

	cout << "Case # 4" << endl;
// c++11 부터 사용 가능한 auto연산자 이용. 이 경우 i는 각 원소를 복사한 변수라서 vector 안에서 원소의 순서를 알수 없음.
	for(auto i : test){
		cout << i << " ";
	}
	cout << endl;

// erase 함수 : 지정 위치의 원소를 제거.
	test.erase(test.begin()+ 2);

	cout << "Case # 5" << endl;
	for(auto i : test){
		cout << i << " ";
	}
	cout << endl;

// 원소별 값을 지정하는 초기화.
	vector<int> insertVector{1,2,3};

// 다른 vector의 값을 insert 하는 방법. 원하는 위치에 두번째 인수(iteraoter) 부터 세번째 인수(iterator)까지를 복사해서 넣어줌.
	test.insert(test.begin()+ 2 , insertVector.begin(), insertVector.end());

	cout << "Case # 6" << endl;
	for(auto i : test){
		cout << i << " ";
	}
	cout << endl;

// erase 함수도 insert 함수와 같이 다수의 값을 삭제 할수 있음.
	test.erase(test.begin()+ 2, test.begin() + 2 +3);

	cout << "Case # 7" << endl;
	for(auto i : test){
		cout << i << " ";
	}
	cout << endl;

//vector도 변수의 사용이 종료되면 (로컬 함수 종료) 컴파일러와 OS가 자동으로 할당 메모리를 반환함. 그래도 다 쓴 벡터는 명시적으로 clear해주는게 도리!!
	insertVector.clear();
	test.clear();
	return 0;
}

코드에 주석을 참조하시면, 초기화나 원소의 추가, 제거하는 법을 배우실 수 있을 실 거예요. 실행 결과는 아래 스냅숏과 같으니 참조하세요. 위 코드를 컴파일할 때 반드시 c++11 이상의 표준 규격을 이용하셔야 합니다. (auto 지원)

그럼 이제 vector의 장점을 알아보죠.

첫 번째 장점은 당연히 위 예제와 같이 쉽게 사용할 수 있다는 것입니다. 일반 배열을 사용하는 것과 같은 문법을 쉽게 사용할 수 있습니다. 거기에 추가로 STL을 사용함으로써 이용 가능한 무수한 표준 함수들을 사용할 수 있습니다. 즉 정렬이나 검색을 위해 추가 함수를 만들 필요 없이 표준 함수를 사용하시면 됩니다.

두 번째 장점은 메모리 사용량이 적습니다. vector를 사용함으로써 추가되는 메모리는 없다고 봐도 무방한 수준입니다.  vector는 동적으로 동작하는 배열이니까. 일반 배열과의 차이는 동적이라는 것뿐이에요. 당연히 일반 배열과 동일한 수준의 메모리를 사용합니다.

 

그럼 간단한 예제로 보여 드리죠. 예제는 랜덤으로 n개의 숫자를 만든 뒤, 그 숫자들을 정렬하겠습니다. n은 입력 인수로 받아 줄 거구요. 랜덤 시드는 항상 1로 동일하게 선택합니다. (혹시 랜덤을 안 배운 분들을 위한 언급. 랜덤 시드가 같으면 랜덤은 항상 같은 숫자를 같을 배열로 반환합니다.)

첫 번째 프로그램은 포인터를 이용합니다. (n이 입력 인수이니 정적인 배열은 사용 불가합니다. ) 정렬 방법은 선택 정렬을 이용합니다. 선택 정렬은 간단하지만 구동 시간이 N^2 만큼 소요되는 방식입니다.

두 번째 프로그램은 vector를 이용합니다. 추가되는 값은 push_back함수로 넣어 주고, 정렬은 <algorithm>을 인클루드 했을 때 사용 가능한 표준 함수인 sort를 이용합니다. sort는 구동 시간 N*log2(N)을 보장하는 최적화된 함수입니다.

혹시 프로그램의 결과를 보고 싶으신 분은 받아서 주석을 삭제한 후 구동시키시면 됩니다.

 

첫 번째 프로그램

#include <iostream>
#include <cstdlib>

using namespace std;

int main(int argc, char ** argv){
	srand(1);

	int n = atoi(argv[1]);

	int * arr;

	arr = new int[n];

	for(int i = 0 ; n > i ; i++){
		arr[i] = rand()%n;
	}

	/*
	cout << "before sort" << endl;
	for(int j = 0 ; i >= j ; j++){
		cout << arr[j] << "\t";
	}
	cout << endl;
	*/

	for(int j = 0 ; n > j ; j++){
		for(int k = j+1 ; n > k ; k++){
			if(arr[j] > arr[k]){
				swap(arr[j], arr[k]);
			}
		}
	}

	/*
	cout << "after sort" << endl;
	for(int j = 0 ; i >= j ; j++){
		cout << arr[j] << "\t";
	}
	cout << endl;
	*/

	delete arr;

	return 0;
}

두 번째 프로그램

#include <vector>
#include <algorithm>
#include <iostream>
#include <cstdlib>

using namespace std;

int main(int argc, char ** argv){
	srand(1);

	int n = atoi(argv[1]);

	vector<int> arr;

	for(int i = 0 ; n > i ; i++){
		arr.push_back(rand()%n);
	}

	/*
	cout << "before sort" << endl;
	for(int j = 0 ; n > j ; j++){
		cout << arr[j] << "\t";
	}
	cout << endl;
	*/

	sort(arr.begin(), arr.end());

	/*
	cout << "after sort" << endl;
	for(int j = 0 ; n > j ; j++){
		cout << arr[j] << "\t";
	}
	cout << endl;
	*/

	arr.clear();

	return 0;
}

일단 코드로 보면 STL을 이용한 장점으로 프로그램이 간결해집니다. 굳이 함수를 만들지 않아도 표준 함수를 이용해 간결한 코드를 만들 수 있죠. 두 번째 이점은 구동 시간입니다. 위에서 언급한 내용이지만 표준 함수 sort는 N * log2(N)의 구동 시간을 보장합니다. 당연히 빠르죠. 아래 스냅숏은 상기 두 프로그램의 구동 결과입니다. 100,000개의 숫자를 생성 후 정렬한다고 했을 때 첫 번째 프로그램과 같이 손수 구현할 때 보다 비교할 수 없이 빠른 걸 보실 수 있죠. 물론 이 비교는 정당하지 않습니다. 논문이나 보고서라면 힙 정렬이나 병합 정렬을 구현해서 비교하는 게 맞겠죠. 그러면 속도 차이는 극복될 것입니다.

그래도 어쨌든 vector는 손쉽게 사용할 수 있고, 성능이 검증된 여러 표준 함수를 사용 할 수 있는 장점이 있습니다.

 

마지막으로 주의점을 언급하려고 합니다. 원래 단점으로 지적하려다가 적합한 예제를 못 만들었어요.....(향후 적합한 예제가 생기면 대체될 수도 있겠네요.)

컴퓨팅 시스템은 메모리에 적재된 데이터를 읽어서 처리합니다. C, C++의 배열은 항상 메모리의 연속된 공간을 요구하고, vector 또한 마찬가지입니다. 즉 100KB의 용량이 필요한 배열이라면 컴퓨팅 시스템은 메모리에서 100KB의 빈 공간을 만들어 할당해 주어야 합니다. 그걸 동적 할당에서는 malloc이나 new를 이용해 할당받죠.

그럼 vector는 어떻게 할까요? 그냥 필요할 때마다 용량을 재할당 받아서 이용합니다. 아래 예제를 보시죠. 이 프로그램은 위 프로그램에서 정렬 부분은 삭제해 버리고 arr [0]의 주소 값을 참조하여 그 값이 변하면 출력시킵니다.

#include <vector>
#include <algorithm>
#include <iostream>
#include <cstdlib>

using namespace std;

int main(int argc, char ** argv){
	srand(1);

	int n = atoi(argv[1]);

	vector<int> arr;

	int * ptr = &arr[0];
	for(int i = 0 ; n > i ; i++){
		arr.push_back(rand()%n);

		if(&arr[0] != ptr){
			ptr = &arr[0];
            cout << "i = " << i << endl;
			cout << "&arr[0] = " <<  ptr << endl;
			cout << "arr.capacity() = " << arr.capacity() << endl;
		}

	}

	arr.clear();

	return 0;
}

이 프로그램의 구동 결과는 아래 스냅숏과 같습니다.

vector의 capacity() 함수는 할당받은 메모리의 크기를 보여 줍니다. (주의 : size는 사용 중인 실제 크기, capacity는 할당받은 크기입니다. 위 결과에서 프로그램의 종료 시 capacity는 16이지만 size는 10입니다.) 결과를 보면 i가 0, 1, 2, 4, 8 일 때 프로그램은 vector의 크기를 재할당 받습니다. 그로 인해 메모리 주소 값도 변하고, 크기도 달라지죠. 과거엔 이런 구조가 문제가 되기도 했습니다. vector를 사용하는 소프트웨어 엔지니어는 이러한 vector의 특성을 인지하고 사용해야 합니다. 특히 vector의 주소 값을 함수의 인자로 사용한다던가 할 경우 실수하면 다른 메모리 값을 참조할 수 있습니다.

 

이상 STL을 배우고 나면 배열을 대체하게 되는 vector에 대해서 알아보았습니다.

 

감사합니다.

반응형