[펌] About STL : C++ STL 프로그래밍(4)-벡터

|
제공 : 한빛 네트워크
저자 : 최흥배
이전기사 :이번 회는 이전 회에 설명한 list와 같은 STL의 컨테이너 라이브러리인 vector에 대해서 이야기합니다. vector는 STL에서 가장 자주 사용합니다. 프로그래밍을 할 때 가장 자주 사용하는 자료구조는 배열입니다. vector는 배열을 대체하여 사용할 수 있습니다. vector는 배열과 비슷한 면이 많아서 STL 컨테이너 중에서 이해하기가 가장 쉽고 또 어디에 사용해야 하는지 알기 쉽습니다. 앞서 연재한 list에 대한 글을 보신 분들은(또는 아시는 분들은) vector와 사용 방법이 비슷한 점이 많아서 list보다 훨씬 더 빠르게 이해하리라 생각합니다. list에서 이미 언급한 몇몇 부분은 다시 언급하지 않으니 list에 대한 글을 보지 않으신 분은 꼭 보시기 바랍니다. 

4.1 vector의 자료구조

처음 말했듯이 vector의 자료구조는 배열과 비슷합니다. Wikipedia에서 배열은 “번호와 번호에 대응하는 데이터로 이루어진 자료구조를 나타냅니다. 일반적으로 배열에는 같은 종류의 데이터가 순차적으로 저장된다’고 설명합니다. 문자 A, B, C, D, E를 배열에 저장한다면 아래 그림과 같이 저장합니다. 

 
[그림 1] A, B, C, D, E가 저장된 배열 

배열의 크기는 고정이지만 vector는 동적으로 변하는 점이 vector와 배열 자료구조의 큰 차이점입니다. 

4.2 배열의 특징

1. 배열의 크기는 고정이다. 
배열은 처음에 크기를 설정하면 이후에 크기를 변경하지 못합니다. 처음 설정한 크기를 넘어서 데이터를 저장할 수 없습니다. [그림 1]의 배열은 A, B, C, D, E만 저장할 수 있게 5개의 크기로 만들어져 있습니다. 배열은 크기가 고정이므로 더 이상 새로운 것을 넣을 수 없습니다. 

2. 중간에 데이터 삽입, 삭제가 용이하지 않다. 
배열은 데이터를 순차적으로 저장합니다. 중간에 데이터를 삽입하면 삽입한 위치 이후의 데이터는 모두 뒤로 하나씩 이동해야 합니다. 또 중간에 있는 데이터를 삭제하면 삭제한 위치 이후의 데이터는 모두 앞으로 하나씩 이동해야 합니다. 

 
[ 그림 2] 중간에 삽입. F를 C와 D 사이에 삽입. D와 E를 뒤로 이동 시킨 후 빈 공간에 넣는다 

 
[그림 3] 중간에 삭제. C를 삭제. 삭제 후 D와 E가 앞으로 이동 

3. 구현이 쉽다. 
배열은 크기가 고정이며 중간 삭제 및 삽입에 대한 특별한 기능이 없는 아주 단순한 자료구조입니다. 제일 처음 프로그래밍을 배울 때 배우는 자료구조가 배열 일 정도로 구현이 쉽습니다. 

4. 랜덤 접근이 가능하다. 
배열은 데이터를 순차적으로 저장하므로 랜덤 접근이 가능합니다. 

4.3 vector를 사용해야 하는 경우

1. 저장할 데이터 개수가 가변적이다. 
배열과 vector의 가장 큰 차이점은 ‘배열은 크기가 고정이고 vector는 크기가 동적으로 변한다’입니다. 저장할 데이터 개수를 미리 알 수 없다면 vector를 사용 하는 편이 좋습니다. 

2. 중간에 데이터 삽입이나 삭제가 없다. 
vector는 배열처럼 데이터를 순차적으로 저장합니다. 중간에 데이터 삭제 및 삽입을 하면 배열과 같은 문제가 발생합니다. vector는 가장 뒤에서부터 데이터를 삭제 하거나 삽입하는 경우에 적합합니다. 

3. 저장할 데이터 개수가 적거나 많은 경우 빈번하게 검색하지 않는다. 
데이터를 순차적으로 저장하므로 많은 데이터를 저장한다면 검색 속도가 빠르지 않습니다. 검색을 자주한다면 map이나 set, hash_map을 사용해야 합니다. 

4. 데이터 접근을 랜덤하게 하고 싶다. 
vector는 배열 같은 특성이 있어서 랜덤 접근이 가능합니다. 특정 데이터가 저장된 위치를 안다면 랜덤 접근을 사용하는 쪽이 성능이 좋고, 사용하기도 간편합 니다. 예를 들면 온라인 게임 제작 시 아이템 번호를 순차적으로 부여한다고 가정합니다. 아이템 데이터를 vector에 저장하면 아이템 개수가 늘어나더라도 코 드를 수정하지 않아도 되며, 아이템 코드 7번은 언제나 7번째 위치에 있으므로 랜덤 접근으로 빠르고 쉽게 접근할 수 있습니다. 위에 열거한 배열의 특징과 vector의 특징을 잘 숙지하여 기존에 배열을 사용한 부분에 vector를 사용하면 배열의 단점을 없앤 유지보수성이 좋은 코드를 만들게 됩니다. 

4.4 vector vs. list

vector 사용법을 보면 list와 비슷한 부분도 있고 다른 부분도 있음을 알게 되리라 생각합니다. vector과 list의 차이점을 잘 이해한 후 올바르게 사용해야 됩니다 . vector와 list의 차이를 정리하면 아래 표와 같습니다. 

vectorList
크기 변경 가능OO
중간 삽입, 삭제 용이XO
순차 접근 가능OO
랜덤 접근 가능OX
[표 1] vector와 list의 차이 

[표 1]을 보시면 아시겠지만 vector와 list의 차이점은 크게 2가지입니다.
  • 중간 삽입, 삭제
  • 랜덤 접근
중간 삽입, 삭제가 없고 랜덤 접근을 자주 해야 된다면 vector가 좋고, 중간 삽입, 삭제가 자주 있으며 랜덤 접근이 필요 없으면 list가 좋습니다. 

[참고]
중간 삽입 삭제가 있다면 무조건 list를 사용해야 할까요? list와 vector의 차이점을 보면 중간 삽입, 삭제가 자주 일어나는 자료구조는 list 사용이 정답입니다. 그렇다고 list 사용이 항상 정답은 아닙니다. 만약 저장하는 데이터의 개수가 적고 랜덤 접근을 하고 싶은 경우에는 vector를 사용해도 좋습니다. 

제가 하는 일을 예를 들면 대부분의 온라인 캐주얼 게임은 방을 만들어서 방에 들어온 유저끼리 게임을 합니다. 방은 유저가 들어 오기도 하고 나가기도 합니 다. 중간 삽입, 삭제가 자주 일어나지만 방의 유저 수는 대부분 적습니다. 이런 경우 중간 삽입, 삭제로 데이터를 이동해도 전체적인 성능 측면에서는 문제가 되지 않습니다. 방에 있는 유저를 저장한 위치를 알고 있다면 유저 데이터에 접근할 때 list는 반복문으로 해당 위치까지 순차적으로 접근해야 하지만 vector는 바로 랜덤 접근이 가능하고 성능면에서도 더 좋습니다. 또한, 방에 있는 모든 유저 데이터에 접근할 때 list는 반복자(Iterator)로 접근하지만, vector는 일반 배열 처럼 접근하므로 훨씬 간단하게 유저 데이터에 접근할 수 있습니다. 

저장하는 데이터 개수가 적은 경우는 대부분 list 보다는 vector를 사용하는 편이 더 좋습니다. vector와 list의 차이점만으로 둘 중 어느 것을 사용할지 선택하기 보다는 전체적인 장, 단점을 파악하고나서 선택하는 것이 좋습니다.

4.5 vector 사용 방법

vector를 사용하려면 vector 헤더 파일을 포함해야 합니다.
#include <vector>
vector 형식은 아래와 같습니다.
vector< 자료 type > 변수 이름
vector를 int 형에 대해 선언했습니다.
vector< int > vector1;
선언 후 vector를 사용합니다. vector도 list처럼 동적 할당이 가능합니다.
vector < 자료 type >* 변수 이름 = new vector< 자료 type >;
vector< int >* vector1 = new vector< int>;

4.5.1 vector의 주요 멤버들

vector 멤버 중 일반적으로 자주 사용하는 멤버들은 아래와 같습니다. 

멤버설명
assign특정 원소로 채운다
at특정 위치의 원소의 참조를 반환
back마지막 원소의 참조를 반환
begin첫 번째 원소의 랜던 접근 반복자를 반환
clear모든 원소를 삭제
empty아무것도 없으면 true 반환
End마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase특정 위치의 원소나 지정 범위의 원소를 삭제
front첫 번째 원소의 참조를 반환
insert특정 위치에 원소 삽입
pop_back마지막 원소를 삭제
push_back마지막에 원소를 추가
rbegin역방향으로 첫 번째 원소의 반복자를 반환
rend역방향으로 마지막 원소 다음의 반복자를 반환
reserve지정된 크기의 저장 공간을 확보
size원소의 개소를 반환
swap두 개의 vector의 원소를 서로 맞바꾼다
[표 2] 자주 사용하는 vector 멤버 

4.5.1.1 기본 사용 멤버 

vector의 가장 기본적인(추가, 삭제, 접근 등) 사용법을 설명 하겠습니다. [표 3]과 [그림 4]를 참고 해주세요 

멤버원형설명
atreference at( size_type _Pos ); 
const_reference at( size_type _Pos ) const;
특정 위치의 원소의 참조를 반환
backreference back( ); 
const_reference back( ) const;
마지막 원소의 참조를 반환
beginconst_iterator begin() const; 
iterator begin();
첫 번째 원소의 랜덤 접근 반복자를 반환
clearvoid clear();모든 원소를 삭제
emptybool empty() const;아무것도 없으면 true 반환
enditerator end( ); 
const_iterator end( ) const;
마지막 원소 다음의(미 사용 영역) 반복자를 반환
frontreference front( ); 
const_reference front( ) const;
첫 번째 원소의 참조를 반환
pop_backvoid pop_back();마지막 원소를 삭제
push_backvoid push_back( const Type& _Val );마지막에 원소를 추가
rbeginreverse_iterator rbegin( ); 
const_reverse_iterator rbegin( ) const;
역방향으로 첫 번째 원소의 반복자를 반환
rendconst_reverse_iterator rend( ) const; 
reverse_iterator rend( );
역방향으로 마지막 원소 다음의 반복자를 반환
sizesize_type size() const;원소의 개수를 반환
[표 3] 추가, 삭제, 접근 등에 사용하는 멤버들 

 
[그림 4] vector의 추가, 삭제, 접근 멤버를 나타내고 있다 

추가 
기본적으로 원소의 마지막 위치에 추가하며 push_back을 사용합니다. 처음이나 중간 위치에 추가할 때는 다음에 소개할 insert를 사용합니다.
vector< int > vector1;
vector1.push_back( 1 );
삭제 
기본적으로 마지막 위치의 원소를 삭제하며 pop_back을 사용합니다. 처음이나 중간에 있는 원소를 삭제할 때는 다음에 소개할 erase를 사용합니다.
vector1.pop_back();
접근 
첫 번째 위치의 반복자를 반환할 때는 begin()을 사용합니다. 첫 번째 원소의 참조를 반환할 때는 front()를 사용합니다.
vector< int >::iterator IterBegin = vector1.begin();
cout << *IterBegin << endl;

int& FirstValue = vector1.front();
const int& refFirstValue = vector1.front();
마지막 다음의 영역(미 사용 영역)을 가리키는 반복자를 반환할 때는 end()를 사용합니다. 마지막 원소의 참조를 반환할 때는 back()을 사용합니다.
;
vector< int >::iterator IterEnd = vector1.end();
for(vector< int >::iterator IterPos = vector1.begin; IterPos != vector1.end();
     ++IterPos )
{
   ……..
}

int& LastValue = vector1.back();
const int& refLastValue = vector1.back();
rbegin()과 rend()는 방향이 역방향이라는 점만 다를 뿐, 나머지는 begin()과 end()와 같습니다. 특정 위치에 있는 원소를 접근할 때는 at()을 사용하면 됩니다.
int& Value1 = vector1.at(0); // 첫 번째 위치
const int Value2 = vector1.at(1); // 두 번째 위치
배열 식 접근도 가능합니다. vector를 사용할 때 보통 이 방식으로 자주 사용합니다.
int Value = vector1[0]; // 첫 번째 위치
모두 삭제 
저장한 모든 데이터를 삭제할 때는 clear()를 사용합니다.
vector1.clear();
데이터 저장 여부 
vector에 저장한 데이터가 있는지 없는지는 empty()로 조사합니다. empty()는 데이터가 있으면 false, 없다면 true를 반환합니다.
bool bEmpty = vector1.empty();
vector에 저장된 원소 개수 알기 
size()를 사용하여 vector에 저장 되어 있는 원소 개수를 조사합니다.
vector< int >::size_type Count = vector1.size();
위에 설명한 멤버들을 사용하는 아래의 예제 코드를 보면서 vector의 기본 사용 방법을 정확하게 숙지해 주세요 

[리스트 1] 온라인 게임의 게임 방의 유저 관리
#include <vector>
#include <iostream>

using namespace std;


// 방의 유저 정보
struct RoomUser
{
	int CharCd; // 캐릭터 코드
	int Level;  // 레벨
};


void main()
{
	// 유저1
	RoomUser RoomUser1;
	RoomUser1.CharCd = 1;	RoomUser1.Level = 10;

	// 유저2
	RoomUser RoomUser2;
	RoomUser2.CharCd = 2;	RoomUser2.Level = 5;
	// 유저3
	RoomUser RoomUser3;
	RoomUser3.CharCd = 3;	RoomUser3.Level = 12;

	// 방의 유저들을 저장할 vector
	vector< RoomUser > RoomUsers;


	// 추가
	RoomUsers.push_back( RoomUser1 );
	RoomUsers.push_back( RoomUser2 );
	RoomUsers.push_back( RoomUser3 );

	// 방에 있는 유저 수
	int UserCount = RoomUsers.size();

	// 방에 있는 유저 정보 출력
	// 반복자로 접근 -  순방향
	for( vector< RoomUser >::iterator IterPos = RoomUsers.begin();
		IterPos != RoomUsers.end();
		++IterPos )
	{
		cout << "유저코드 : " << IterPos->CharCd << endl;
		cout << "유저레벨 : " << IterPos->Level << endl;
	}
	cout << endl;
	
	// 반복자로 접근- 역방향
	for( vector< RoomUser >::reverse_iterator IterPos = RoomUsers.rbegin();
		IterPos != RoomUsers.rend();
		++IterPos )
	{
		cout << "유저코드: " << IterPos->CharCd << endl;
		cout << "유저레벨: " << IterPos->Level << endl;
	}
	cout << endl;

	// 배열 방식으로 접근
	for( int i = 0; i < UserCount; ++i )
	{
		cout << "유저 코드 : " << RoomUsers[i].CharCd << endl;
		cout << "유저 레벨 : " << RoomUsers[i].Level << endl;
	}
	cout << endl;

	// 첫 번째 유저 데이터 접근
	RoomUser& FirstRoomUser = RoomUsers.front();
	cout << "첫 번째 유저의 레벨 : " << FirstRoomUser.Level << endl << endl;

	RoomUser& LastRoomUser = RoomUsers.back();
	cout << "마지막 번째 유저의 레벨: " << LastRoomUser.Level << endl << endl;

	// at을 사용하여 두 번째 유저의 레벨을 출력
	RoomUser& RoomUserAt = RoomUsers.at(1);
	cout << "두 번째 유저의 레벨: " << RoomUserAt.Level << endl << endl;

	// 삭제
	RoomUsers.pop_back();

	UserCount = RoomUsers.size();
	cout << "현재 방에 있는 유저 수: " << UserCount << endl << endl;


	// 아직 방에 유저가 있다면 모두 삭제한다.
	if( false == RoomUsers.empty() )
	{
		RoomUsers.clear();
	}

	UserCount = RoomUsers.size();
	cout << "현재 방에 있는 유저 수: " << UserCount << endl;
}
결과 

 

혹시 랜덤 접근이 무엇인지 잘 이해하지 못한 분은 [리스트 1]의 예제 코드에서 배열 방식으로 접근하는 부분을 잘 보세요. 배열처럼 접근 하는 것을 랜덤 접근 이 가능하다고 말합니다. 랜덤 접근이 안 되는 list에서는 오직 반복자로 순차 접근만 가능합니다. 

4.5.1.2 insert 

insert는 지정된 위치에 삽입하며, 세 가지 방식이 있습니다. list의 insert와 사용 방법이 같습니다. 세 가지 원형은 각각 지정한 위치에 삽입, 지정한 위치에 지 정한 개수만큼 삽입, 지정한 위치에 지정 범위에 있는 것을 삽입합니다. vector의 insert를 사용할 때에는 삽입한 위치 이후의 원소들이 모두 뒤로 이동함을 꼭 숙지하셔야 됩니다.
원형 : iterator insert( iterator _Where, const Type& _Val );
         void insert( iterator _Where, size_type _Count, const Type& _Val );
         template<class InputIterator> void insert( iterator _Where, InputIterator _First, 
                                                                         InputIterator _Last );
 
[그림 5] vector의 insert 

첫 번째 insert는 지정한 위치에 데이터를 삽입합니다.
vector< int >::iterator iterInsertPos = vector1.begin();
vector1.insert( iterInsertPos, 100 );
이 코드는 100을 첫 번째 위치에 삽입합니다. 두 번째 insert는 지정한 위치에 데이터를 횟수만큼 삽입합니다.
iterInsertPos = vector1.begin();
++iterInsertPos;
vector1.insert( iterInsertPos, 2, 200 );
두 번째 위치에 200을 두 번 추가합니다. 세 번째 insert는 지정한 위치에 복사 할 vector의 (복사하기를 원하는 영역의)시작과 끝 반복자가 가리키는 영역의 모 든 요소를 삽입합니다.
vector< int > vector2;
list2.push_back( 500 );
list2.push_back( 600 );
list2.push_back( 700 );

iterInsertPos = vector1.begin();
vector1.insert( ++iterInsertPos, vector2.begin(), vector2.end() );
vector1의 두 번째 위치에 vector2의 모든 요소를 삽입합니다. 위에서 설명한 insert의 세 가지 방법을 사용한 전체 코드입니다. 참고로 이 예제는 이전 회의 list 의 insert에서 소개했던 코드와 같습니다. 오직 list 대신 vector를 사용했다는 것만 다릅니다. 

[리스트 2] insert 사용
void main()
{
	vector< int > vector1;

	vector1.push_back(20);
	vector1.push_back(30);


	cout << "삽입 테스트 1" << endl;

	// 첫 번째 위치에 삽입한다.
	vector< int >::iterator iterInsertPos = vector1.begin();
	vector1.insert( iterInsertPos, 100 );
	
	// 100, 20, 30 순으로 출력한다.
	vector< int >::iterator iterEnd = vector1.end();
	for(vector< int >::iterator iterPos = vector1.begin(); 
		iterPos != iterEnd; 
		++iterPos )
	{
		cout << "vector1 : " << *iterPos << endl;
	}


	cout << endl << "삽입 테스트 2" << endl;

	// 두 번째 위치에 200을 2개 삽입한다.
	iterInsertPos = vector1.begin();
	++iterInsertPos;
	vector1.insert( iterInsertPos, 2, 200 );
	
	// 100, 200, 200, 20, 30 순으로 출력한다.
	iterEnd = vector1.end();
	for(vector< int >::iterator iterPos = vector1.begin(); 
		iterPos != iterEnd; 
		++iterPos )
	{
		cout << "vector1 : " << *iterPos << endl;
	}


	cout << endl << "삽입 테스트 3" << endl;

	vector< int > vector2;
	vector2.push_back( 1000 );
	vector2.push_back( 2000 );
	vector2.push_back( 3000 );

	// 두 번째 위치에 vecter2의 모든 데이터를 삽입한다.
	iterInsertPos = vector1.begin();
	vector1.insert( ++iterInsertPos, vector2.begin(), vector2.end() );
	
	// 100, 1000, 2000, 3000, 200, 200, 20, 30 순으로 출력한다.
	iterEnd = vector1.end();
	for(vector< int >::iterator iterPos = vector1.begin(); 
		iterPos != iterEnd; 
		++iterPos )
	{
		cout << "vector1 : " << *iterPos << endl;
	}
}
결과 

 

4.5.1.3 erase 

반복자로 특정 위치의 요소를 삭제할 때는 erase를 사용합니다. 사용 방식은 두 가지가 있습니다. 하나는 지정한 위치의 요소를 삭제하고, 다른 하나는 지정한 범위의 요소를 삭제합니다. 마지막 위치 이외의 곳에서 erase를 할 때는 삭제한 위치 이후의 모든 원소들이 앞으로 이동한다는 것을 꼭 숙지하셔야 됩니다.
원형 : iterator erase( iterator _Where );
         iterator erase( iterator _First, iterator _Last );
 
[그림 6] vector의 erase 

첫 번째 erase는 지정한 위치의 요소를 삭제합니다. 다음은 첫 번째 요소를 삭제하는 코드입니다.
vector1.erase( vector1.begin() );
두 번째 erase는 지정한 반복자 요소만큼 삭제합니다. 다음 코드는 vector1의 첫 번째 요소에서 마지막까지 모두 삭제합니다.
vector1.erase( vector1.begin(), vector1.end() );
다음은 erase 사용을 보여주는 예제입니다. 

[리스트 3] erase 사용
void main()
{
	vector< int > vector1;

	vector1.push_back(10);
	vector1.push_back(20);
	vector1.push_back(30);
	vector1.push_back(40);
	vector1.push_back(50);

	int Count = vector1.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	cout << endl;


	cout << "erase 테스트 1" << endl;

	// 첫 번째 데이터 삭제
	vector1.erase( vector1.begin() );
	
	// 20, 30, 40, 50 출력
	Count = vector1.size();		
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	

	cout << endl << "erase 테스트" << endl;

	// 첫 번째 데이터에서 마지막까지 삭제한다.
	vector< int >::iterator iterPos = vector1.begin();
	vector1.erase( iterPos, vector1.end() );
	
	if( vector1.empty() )
	{
		cout << "vector1에 아무 것도 없습니다" << endl;
	}
}
결과 

 

4.5.1.4 assign 

vector를 어떤 특정 데이터로 채울 때는 assign을 사용하면 됩니다. 사용 방식은 두 가지가 있습니다. 첫 번째는 특정 값으로 채우는 방법이고, 두 번째는 다른 vector의 반복자로 지정한 영역에 있는 원소로 채우는 방법입니다. 만약 assign을 사용한 vector에 이미 데이터가 있다면 기존의 것은 모두 지우고 채웁니다.
원형 : void assign( size_type _Count, const Type& _Val );
         template<class InputIterator> void assign( InputIterator _First, InputIterator _Last );
 
[그림 7] assign 

첫 번째 assign은 지정 데이터를 지정 개수만큼 채워줍니다. 숫자 4를 7개 채웁니다.
vector1.assign( 7, 4 );
두 번째 assign은 다른 vector의 반복자로 지정한 영역으로 채워줍니다.
vector1.erase( vector1.begin(), vector1.end() );
다음은 assign을 사용법을 보여주는 예제입니다. 

[리스트 4] assign
void main()
{
	vector< int > vector1;
	
	// 4를 7개 채운다.
	vector1.assign( 7, 4 );
	
	int Count = vector1.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	cout << endl;



	vector< int > vector2;
	vector2.push_back(10);
	vector2.push_back(20);
	vector2.push_back(30);
	

	// vector2의 요소로 채운다
	vector1.assign( vector2.begin(), vector2.end() );
	Count = vector1.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	cout << endl;
}
결과 

 

4.5.1.5 reserve 

vector는 사용할 메모리 영역을 처음 선언할 때 정해진 값만큼 할당한 후 이 크기를 넘어서게 사용하면 현재 할당한 크기의 2배의 크기로 재할당합니다. vector에 어느 정도의 데이터를 저장할지 가늠할 수 있고, vector 사용 도중에 재할당이 일어나는 것을 피하려면 사용할 만큼의 크기를 미리 지정해야 합니다. 참고로 reserve로 지정할 수 있는 크기는 vector에서 할당하는 최소의 크기보다는 커야 합니다.
원형 : void reserve( size_type _Count );
 
[그림 8] reserve 

10개의 원소를 채울 수 있는 공간 확보
vector1.reserve( 10 );
4.5.1.6 swap 

vector1과 vector2가 있을 때 두 개의 vector간에 서로 데이터를 맞바꾸기를 할 때 사용합니다.
원형 : void swap( vector<Type, Allocator>& _Right );
         friend void swap( vector<Type, Allocator >& _Left, vector<Type, Allocator >& _Right );
 
[그림 9] swap 

vector1과 vector2를 swap
vector1.swap( vector2 );
swap을 사용하는 예제입니다. 

[리스트 5] Swap
void main()
{
	vector< int > vector1;
	vector1.push_back(1);
	vector1.push_back(2);
	vector1.push_back(3);

	vector< int > vector2;
	vector2.push_back(10);
	vector2.push_back(20);
	vector2.push_back(30);
	vector2.push_back(40);
	vector2.push_back(50);


	int Count = vector1.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	cout << endl;


	Count = vector2.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 2 : " << vector2[i] << endl;
	}
	cout << endl;
	cout << endl;

	cout << "vector1과vector2를swap" << endl;

	vector1.swap(vector2);

	Count = vector1.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 1 : " << vector1[i] << endl;
	}
	cout << endl;


	Count = vector2.size();
	for( int i = 0; i < Count; ++i )
	{
		cout << "vector 2 : " << vector2[i] << endl;
	}
}
결과 

 

vector 중에서 가장 많이 사용하는 멤버를 중심으로 설명 하였습니다. vector의 모든 멤버를 설명하지는 않았으니 소개하지 않은 나머지 멤버까지 알고 싶다면 마이크로소프트의 MSDN에 나와 있는 것을 참고해 주세요. http://msdn.microsoft.com/en-us/library/sxcsf7y7.aspx 

앞 회의 list 글을 보신 분들은 이번에 설명한 vector에서 소개한 front(), push_back(), pop_back(), erase() 등이 list의 멤버들과 사용 방법이나 결과가 같음을 알 수 있습니다. 이것이 STL를 사용하여 얻는 장점 중의 하나입니다. 

STL에서 제공하는 컨테이너들은 서로 특성은 다르지만 사용 방법과 결과가 같기 때문에 하나만 잘 알며 다른 것들도 쉽게 배울 수 있습니다. 만약 STL의 컨테 이너를 사용하지 않고 독자적으로 구현하여 사용한다면 각각 사용 방법이 달라서 사용 방법을 배울 때마다 STL보다 더 많은 시간이 필요할 것이며, 함수 이름 을 보고 어떤 동작을 할지 각각의 라이브러리마다 숙지해야 하므로 유지보수에 좋지 않습니다. 

vector는 배열과 비슷하고 사용하기 편리하여 많은 곳에서 사용합니다. 그러나 vector의 특성을 제대로 이해하지 못하고 잘못된 곳에 사용하면 심각한 성능 저 하가 일어날 수 있습니다(많은 데이터를 저장하고 있으며 빈번하게 중간에서 삽입, 삭제를 할 때). 그러니 꼭 적합한 장소에 사용해야 합니다. 

과제 

1. 이전 회의 글 중 ‘3.5 list를 사용한 스택’에서 list를 사용하여 LIFO 방식으로 스택을 만든 예제가 있는데 이것을 vector를 사용하여 만들어 보세요 

2. ‘카트 라이더’와 같이 방을 만들어서 게임을 하는 온라인 게임에서 방에 있는 유저를 관리하는 부분을 vector를 사용하여 만들어 보세요. 기본적인 클래스 선언은 제시할 테니 구현만 하면 됩니다.
// 유저 정보
struct UserInfo
{
	char acUserName[21]; // 이름	
	int	Level;       // 레벨  
	int Exp;             // 경험치   
};

// 게임 방의 유저를 관리하는 클래스
// 방에는 최대 6명까지 들어 갈 수 있다.
// 방에 들어 오는 순서 중 가장 먼저 들어 온 사람이 방장이 된다.
class GameRoomUser
{
public:
	GameRoomUser();
	~GameRoomUser();

	// 방에 유저 추가
	bool AddUser( UserInfo& tUserInfo );

	// 방에서 유저 삭제. 
	// 만약 방장이 나가면 acMasterUserName에 새로운 방장의 이름을 설정 해야 된다.
	bool DelUser( char* pcUserName );

	// 방에 유저가 없는 지조사. 없으면 true 반환
	bool IsEmpty();

	// 방에 유저가 꽉 찼는지 조사. 꽉 찼다면 true 반환
	bool IsFull();

	// 특정 유저의 정보
	UserInfo& GetUserOfName( char* pcName );

	// 방장의 유저 정보
	UserInfo& GetMasterUser();

	// 가장 마지막에 방에 들어 온 유저의 정보
	UserInfo& GetUserOfLastOrder();

	// 특정 순서에 들어 온 유저를 쫒아낸다.
	bool BanUser( int OrderNum );

	// 모든 유저를 삭제한다.
	void Clear();
	
private:
	vector< UserInfo > Users;
	char acMasterUserName[21]; // 방장의 이름

};
출처 : http://www.hanb.co.kr/


Trackback 0 And Comment 0

[펌] About STL : C++ STL 프로그래밍(3) - 연결 리스트

|

제공 : 한빛 네트워크
저자 : 최흥배
이전기사 :

이번 회부터는 본격적으로 STL에 대해서 이야기합니다. STL은 C++ 템플릿을 사용해 만든 표준 라이브러리입니다. 그러니 템플릿에 대해서 아직 잘 모르시는 분들은 앞에 연재한 템플릿에 대한 글을 읽어보시기를 권합니다. 일반적으로 STL 중에서 가장 많이 사용하는 라이브러리는 컨테이너 라이브러리입니다. 컨테이너는 말 그대로 무엇인가를 담는 것입니다. 컨테이너는 int나 float 등의 기본 자료 형이나 구조체, 클래스같은 유저 정의 자료 형을 담습니다. STL의 컨테이너는 list, vector, deque, map, set이 있습니다. 이번 회는 list에 대해서 이야기합니다.

list의 자료구조

list는 자료구조 중 '연결 리스트'를 템플릿으로 구현한 것입니다. 그래서 list를 알려면 '연결 리스트'라는 자료구조의 이해가 꼭 필요합니다. 연결 리스트는 단어 그 자체로 해석하면 "(무엇인가)서로 연결 되어 줄지어 있다"라고 말할 수 있습니다. 말보다는 그림을 보는 것이 이해하기 쉬울 테니 아래 그림을 봐 주세요. 

그림1
그림 1. 연결 리스트

연결 리스트의 특징

1. 고정 길이인 배열에 비해 길이가 가변적이다.
배열은 처음에 설정한 크기 이외에는 더 이상 데이터를 담을 수 없지만 연결 리스트는 동적으로 크기를 변경 할 수 있습니다. 

2. 중간에 데이터 삽입, 삭제가 용이하다.
데이터를 중간에 삽입할 때 배열은 <그림 1>에서 B와 C사이에 새로운 데이터를 넣는다면 <그림 2>와 같이 C 이후의 데이터를 모두 뒤로 이동 해야 합니다. 그러나 연결 리스트는 <그림 3>과 같이 B와 C사이에 넣으면서 연결 고리만 바꾸면 됩니다. 

그림2
그림 2. 배열에서 데이터 삽입하기 

그림3
그림 3. 연결 리스트에서 데이터 삽입하기 

<그림 2>의 B를 삭제 하면 배열은 C 이후의 모든 데이터를 앞으로 이동해야 합니다. 그러나 연결 리스트는 <그림 4>와 같이 B를 삭제하고 B의 연결 고리를 없애면 됩니다. 

그림4
그림 4. 연결 리스트에서 데이터 삭제하기 

이렇게 연결 리스트는 배열에 비해서 크기가 가변적이고, 중간에 데이터 삭제와 삽입이 용이하다는 장점이 있습니다. 그렇지만 단점으로는 배열에 비해서 데이터의 삽입과 삭제를 구현하기 어렵고 내부 절차가 복잡합니다. 배열은 랜덤하게 접근할 수 있지만 연결 리스트는 랜덤하게 접근할 수 없습니다. 연결 리스트는 특징을 잘 파악한 후 알맞은 곳에 사용해야 됩니다.

STL list를 사용하면 좋은 점

STL을 사용하지 않는다면 C/C++ 언어, 자료구조를 공부하고 필요한 자료구조를 직접 만들어 사용해야 합니다. 직접 만들어 사용하면 여러 번 되풀이(프로젝트나 회사가 바뀌면)하여 만들어야 하므로 불필요한 시간을 소비하고, 연결 리스트 자료구조를 잘못 구현하여 버그를 만들 위험이 있고, 개인마다 구현 방법이 다르므로 사용이나 유지보수 측면에서 불편합니다. 

그러나 STL list(이하 list)를 사용하면 연결 리스트를 따로 만들어야 하는 시간을 절약할 수 있고, 이미 검증되어 있으므로 안전하고, 표준 라이브러리이므로 사용 방법이 언제나 같아서 사용 및 유지보수가 좋아집니다. 

다만, list를 사용할 때는 특성을 잘 파악하여 올바르게 사용해야 합니다. list를 적합하지 않은 곳에 사용하면 성능의 하락 및 시스템 에러를 유발할 위험이 생깁니다.

STL에 버그가 있다?
현업에 일하는 분 중 STL을 안 쓰는 분도 있습니다. STL을 사용하지 않는 이유가 STL을 사용했을 때 잘 알 수 없는 문제가 발생했는데 STL을 사용하지 않으니 괜찮아졌다는 이유로 STL에 버그가 있다고 생각하는 분이 있습니다. 제가 생각하기에는 STL의 버그가 아닌 다른 곳에서 발생한 문제이던가 STL의 특징을 제대로 파악하지 못하고 사용해서 일어난 문제라고 생각합니다. 만약 정말 STL에 버그가 있다면 이런 중요한 문제는 널리 알려서 다른 프로그래머들에게 도움을 주고 큰 버그를 찾은 스타(?)가 되어야 하겠죠.

list를 사용해야 하는 경우

1. 저장할 데이터 개수가 가변적이다.
저장할 데이터 개수가 정해져 있지 않은 경우 배열은 설정된 크기를 넘어가면 데이터가 넘쳐서 실행 도중 프로그램 오류가 발생하므로 코드를 수정 후 재컴파일해야 됩니다. 그렇다고 배열에 설정된 크기가 변할 때마다 재컴파일하는 것을 방지하려고 넉넉한 크기로 큰 배열을 만든다면 메모리 낭비가 발생합니다. 그러나 list를 사용하면 저장 공간의 크기가 자동으로 변하므로 유연하게 사용할 수 있습니다.

대형 프로그램을 만들어 보신적이 없는 분들은 컴파일에 걸리는 시간은 짧은 시간이라고 생각하여 컴파일을 자주 하는 것에 대한 문제를 느끼지 못할 수도 있습니다. 그러나 일반적으로 콘솔이나 PC 게임은 클라이언트 프로그램을 ReBuild 하는데 걸리는 시간은 15 ~ 30분 이상 걸리는 경우가 많습니다.



2. 중간에 데이터 삽입이나 삭제가 자주 일어난다.
MMORPG 게임은 지도가 아주 크고 게임상에서 어떤 캐릭터의 행동에 대한 정보를 근처의 클라이언트에게만 통보하므로 지도를 작은 단위로(보통 사각형으로) 나눈 후 같은 단위에 포함 되어 있는 클라이언트와 그 단위 근처의 클라이언트에게만 통보합니다. 지도를 작은 단위로 분할하여 해당 영역에 들어오는 유저는 저장하고 나가는 유저는 삭제를 해야 합니다. 이와 같이 빈번하게 삽입과 삭제가 일어나는 곳에 list를 사용합니다. 

그림5
그림 5. 하나의 지도로 접속한 클라이언트간의 인접 위치를 관리하는 것은 너무 비효율적이므로 오른쪽과 같이 지도를 작은 단위로 나눈 후 접속한 클라이언트를 단위 별로 관리한다. 

3. 저장할 데이터 개수가 많으면서 검색을 자주 한다면 다른 컨테이너 라이브러리를 사용해야 한다.
아주 많은 데이터를 저장하면서 특정 데이터를 자주 검색해야 할 때 list를 사용하면 검색 속도가 많이 느려지므로 이런 경우에는 map이나 set, hash_map을 사용해야 합니다. 

4. 데이터를 랜덤하게 접근하는 경우가 많지 않다.
배열은 랜덤 접근이 가능하나 list는 순차 접근만 가능합니다. 그래서 저장된 위치를 알더라도 반복자(Iterator)(아래에 설명하겠습니다)를 통해서 접근해야 합니다. 아이템을 자주 사용하는 온라인 게임에서는 아이템 사용 시 아이템 정보에 빈번하게 접근하므로 성능을 위해 메모리 낭비를 감수하고 배열로 데이터를 저장해서 랜덤 접근을 사용하게 합니다.

list 사용 방법

list를 사용하려면 list 헤더 파일을 포함해야 합니다.

#include <list>

list 형식은 아래와 같습니다.

list< 자료 type > 변수 이름 

list를 int 형에 대해 선언했습니다.

list< int > list1;

선언 후에는 리스트를 사용하면 됩니다. 물론, 동적 할당도 가능합니다.

list < 자료 type >* 변수 이름 = new list< 자료 type >;
list< int >* list2 = new list< int>;

STL의 namespace

위에서는 list를 바로 사용했는데 이렇게 사용하려면 STL의 namespace를 선언해야 합니다.

using namespace std;

위와 같이 namespace를 선언하지 않고 list를 사용하려면 STL 라이브러리 앞에 namespace를 적어 줘야 합니다.

std::list< int > list;

반복자(Iterator)

list에 저장된 데이터에 접근하려면 반복자를 사용해야 하므로 list를 설명하기 전에 반복자에 대해서 간단하게 이야기합니다. 반복자는 포인터의 일반화된 개념이라고 봐도 됩니다. STL 컨테이너에 저장된 데이터를 순회할 수 있으며 컨테이너에서 특정 위치를 가리킵니다. 포인터와 비슷하게 ++과 --로 이동하고 대입과 비교도 가능합니다. 그리고 각 컨테이너는 컨테이너 전용의 반복자를 구현하고 있습니다. 반복자의 선언 형식은 다음과 같습니다.

STL의 컨테이너 < 자료 type >::iterator 변수 이름

반복자 사용에 대해서 예를 들어 보겠습니다. 

그림6
그림 6. 순 방향의 앞과 끝 반복자, 역 방향의 앞과 끝 반복자 

아래에 begin(), end(), rbegin(), rend()를 설명할 때 <그림 6>을 참고하세요. 설명을 위해 아래와 같이 list1을 선언합니다.

list< int > list1;

begin()
첫 번째 요소를 가리키는 반복자를 반환합니다.

예) list< int >::iterator iterFirst = list1.begin();

end()
마지막 요소를 가리킵니다. 주의할 점은 begin()과 달리 end()는 마지막 요소 바로 다음을 가리킵니다. 즉 사용할 수 없는 영역을 가리키므로 end() 위치의 반복자는 사용하지 못합니다.

예) list< int >::iterator iterEnd = list1.end();

for문에서 list에 저장된 모든 요소에 접근하려면 begin()과 end() 반복자를 사용하면 됩니다.

for( list< int >::iterator iterPos = list1.begin(); iterPos != list1.end(); ++iterPos )
{
  cout << "list1의 요소 : " << *iterPos << endl;
}

list< int >::iterator iterPos는 list에 정의된 반복자를 가져오며, list< int >::iterator iterPos = list1.begin();은 list1의 첫 번째 요소를 가리킵니다. iterPos != list1.end();는 반복자가 end()를 가리키면 for 문을 빠져 나오게 합니다. ++iterPos는 반복자를 하나씩 이동 시킵니다. 

rbegin()
begin()와 비슷한데 다른 점은 역 방향으로 첫 번째 요소를 가리킨다는 것입니다. 그리고 사용하는 반복자도 다릅니다.

예) list::reverse_iterator IterPos = list1->rbegin();

rend()
end()와 비슷한데 다른 점은 역 방향으로 마지막 요소 다음을 가리킨다는 것입니다.

예) list::reverse_iterator IterPos = list1.rend();

반복문에서 rbegin()과 rend()를 사용하여 list1의 각 데이터에 접근한다면 아래처럼 사용하면 됩니다.

for( list::reverse_iterator IterPos = list1.rbegin(); IterPos != list1.rend(); ++IterPos )
{
  cout << "역 방향 list1의 요소 : " << *IterPos << endl;
}

그럼 이제 본격적으로 list의 주요 멤버들의 사용 법에 대해서 설명합니다.

list의 주요 멤버들

표1
표 1. 자주 사용하는 list 멤버 

그럼 각 멤버들의 사용법에 대해서 설명하겠습니다. 아래의 그림도 참조하세요 

그림7
그림 7. list의 앞과 뒤 추가 삭제 및 접근 

표2
표 2. push_front, pop_front, push_back, pop_back, front, back, clear, empty, size의 원형 및 설명 

위에 설명한 것으로는 아직 감이 서지 않는 분도 있을 테니 위에 설명한 것을 사용하는 예제 코드를 봐 주세요. 아래의 코드는 게임에서 사용하는 아이템의 정보를 list 컨테이너를 사용하여 아이템 정보를 앞과 뒤에 추가 및 삭제를 하고 front, back를 사용하여 저장한 아이템 요소를 출력합니다. 

#include <iostream>
#include <list>

using namespace std;

// 아이템 구조체
struct Item
{
  Item( int itemCd, int buyMoney )
  {
    ItemCd = itemCd;
    BuyMoney = buyMoney;
  }

  int ItemCd;  // 아이템코드
  int BuyMoney;  // 판매금액
};

void main()
{
  list< Item > Itemlist;

  // 앞에 데이터 추가
  Item item1( 1, 2000 );
  Itemlist.push_front( item1 );

  Item item2( 2, 1000 );
  Itemlist.push_front( item2 );

  // 뒤에 데이터 추가
  Item item3( 3, 3000 );
  Itemlist.push_back( item3 );

  Item item4( 4, 4500 );
  Itemlist.push_back( item4 );

  // 아이템 코드 번호가 2, 1, 3, 4의 순서로 출력된다.
  list< Item >::iterator iterEnd = Itemlist.end();
  for(list< Item >::iterator iterPos = Itemlist.begin(); 
    iterPos != iterEnd; 
    ++iterPos )
  {
    cout << "아이템 코드 : " << iterPos->ItemCd << endl;
  }

  // 앞에 있는 데이터를 삭제한다.
  Itemlist.pop_front();

  // 앞에 있는 데이터의 참조를 반환한다.
  Item front_item = Itemlist.front(); 
  // 아이템 코드 1이 출력된다.
  cout << "아이템 코드 : " << front_item.ItemCd << endl;


  // 마지막에 있는 데이터를 삭제한다.
  Itemlist.pop_back();

  // 마지막에 있는 데이터의 참조를 반환한다.
  Item back_item = Itemlist.back();
  // 아이템 코드 3이 출력된다.
  cout << "아이템 코드 : " << back_item.ItemCd << endl;

  // 저장된 데이터가 있는가?
  if( false == Itemlist.empty() )
  {
    list< Item >::size_type Count = Itemlist.size();
    cout << "남아 있는 아이템 개수: " << Count << endl;
  }

  // 모든 데이터를 지운다.
  Itemlist.clear();
  list< Item >::size_type Count = Itemlist.size();
  cout << "남아 있는 아이템 개수: " << Count << endl;
}

결과 

결과1 

그럼 계속 해서 <표 1>에 소개된 list 멤버들의 설명을 계속 하겠습니다. 

insert 

insert는 지정된 위치에 삽입하며, 세 가지 방식이 있습니다. 세 가지 원형은 각각 지정된 위치에 삽입, 지정된 위치에 지정된 개수만큼 삽입, 지정된 위치에 지정 범위에 있는 것을 삽입합니다. 아래 그림을 참고하세요

원형 : iterator insert( iterator _Where, const Type& _Val );
       void insert( iterator _Where, size_type _Count, const Type& _Val );
       template void insert( iterator _Where, 
          InputIterator _First, InputIterator _Last );

그림8
그림 8. insert의 세 가지 방법 

첫 번째 insert는 지정한 위치에 데이터를 삽입합니다.

list< int >::iterator iterInsertPos = list1.begin();
list1.insert( iterInsertPos, 100 );

이 코드는 100을 첫 번째 위치에 삽입합니다.
두 번째 insert는 지정한 위치에 데이터를 횟수만큼 삽입합니다.

iterInsertPos = list1.begin();
++iterInsertPos;
list1.insert( iterInsertPos, 2, 200 );

list1의 두 번째 위치에 200을 두 번 추가합니다.
세 번째 insert는 지정한 위치에 복사 할 list의 시작과 끝 반복자가 가리키는 요소를 삽입합니다.

list< int > list2;
list2.push_back( 1000 );
list2.push_back( 2000 );
list2.push_back( 3000 );

iterInsertPos = list1.begin();
list1.insert( ++iterInsertPos, list2.begin(), list2.end() );

list1의 두 번째 위치에 list2의 모든 요소를 삽입합니다.
아래는 위에서 설명한 insert의 세 가지 방법을 사용한 전체 코드입니다. 

#include <iostream>
#include <list>

using namespace std;


void main()
{
  list< int > list1;

  list1.push_back(20);
  list1.push_back(30);


  cout << "삽입 테스트 1" << endl;

  // 첫 번째 위치에 삽입한다.
  list< int >::iterator iterInsertPos = list1.begin();
  list1.insert( iterInsertPos, 100 );
  
  // 100, 20, 30 순으로 출력된다.
  list< int >::iterator iterEnd = list1.end();
  for(list< int >::iterator iterPos = list1.begin(); 
    iterPos != iterEnd; 
    ++iterPos )
  {
    cout << "list 1 : " << *iterPos << endl;
  }


  cout << endl << "삽입 테스트 2" << endl;

  // 두 번째 위치에 200을 2개 삽입한다.
  iterInsertPos = list1.begin();
  ++iterInsertPos;
  list1.insert( iterInsertPos, 2, 200 );
  
  // 100, 200, 200, 20, 30 순으로출력된다.
  iterEnd = list1.end();
  for(list< int >::iterator iterPos = list1.begin(); 
    iterPos != iterEnd; 
    ++iterPos )
  {
    cout << "list 1 : " << *iterPos << endl;
  }


  cout << endl << "삽입 테스트 3" << endl;

  list< int > list2;
  list2.push_back( 1000 );
  list2.push_back( 2000 );
  list2.push_back( 3000 );

  // 두 번째 위치에 list2의 모든 데이터를 삽입한다.
  iterInsertPos = list1.begin();
  list1.insert( ++iterInsertPos, list2.begin(), list2.end() );
  
  // 100, 1000, 2000, 3000, 200, 200, 20, 30 순으로출력된다.
  iterEnd = list1.end();
  for(list< int >::iterator iterPos = list1.begin(); 
    iterPos != iterEnd; 
    ++iterPos )
  {
    cout << "list 1 : " << *iterPos << endl;
  }
}

결과 

결과2 

erase 

erase는 지정된 범위에 있는 데이터 삭제하며, 두 가지 방식이 있습니다. 하나는 지정된 위치의 데이터를 삭제하고, 다른 하나는 지정된 범위의 데이터를 삭제합니다.

원형 : iterator erase( iterator _Where );
       iterator erase( iterator _First, iterator _Last );

그림9
그림 9. erase의 두 가지 방법 

첫 번째 erase는 지정한 위치의 요소를 삭제합니다. 다음은 첫 번째 요소를 삭제하는 코드입니다.

list1.erase( list1.begin() );

두 번째 erase는 지정한 반복자 요소만큼 삭제합니다. 다음 코드는 list1의 두 번째 요소에서 마지막까지 모두 삭제합니다.

list< int >::iterator iterPos = list1.begin();
++iterPos;
list1.erase( iterPos, list1.end() );

아래는 erase의 두 가지 사용 방법을 보여주는 전체 코드입니다. 

void main()
{
  list< int > list1;

  list1.push_back(10);
  list1.push_back(20);
  list1.push_back(30);
  list1.push_back(40);
  list1.push_back(50);

  cout << "erase 테스트 1" << endl;

  // 첫 번째 데이터 삭제
  list1.erase( list1.begin() );

  // 20, 30, 40, 50 출력
  list< int >::iterator iterEnd = list1.end();
  for(list< int >::iterator iterPos = list1.begin(); 
    iterPos != iterEnd; 
    ++iterPos )
  {
    cout << "list 1 : " << *iterPos << endl;
  }

  cout << endl << "erase 테스트2" << endl;

  // 두 번째 데이터에서 마지막까지 삭제한다.
  list< int >::iterator iterPos = list1.begin();
  ++iterPos;
  list1.erase( iterPos, list1.end() );
  
  // 20 출력
  iterEnd = list1.end();
  for(list< int >::iterator iterPos = list1.begin(); 
    iterPos != iterEnd; 
    ++iterPos )
  {
    cout << "list 1 : " << *iterPos << endl;
  }
}

결과 

결과3

list 반복자의 랜덤 접근
위 에서 두 번째 위치에 접근하기 위해 ++iterPos를 사용했습니다. 만약 세 번째 위치로 이동하려면 한 번 더 ++iterPos를 해야 합니다. list는 랜덤 접근이 안 되므로 원하는 위치까지 하나씩 이동해야 합니다. 그러나 vector와 같이 랜덤 접근이 가능한 컨테이너는 다음 코드처럼 바로 접근할 수 있습니다.
iterPos = vector.begin() + 3;

반복문에서 list의 데이터를 삭제하면서 반복하는 경우 조심하지 않으면 버그가 발생합니다. 아래의 코드를 잘 봐주세요. 

#include <iostream>
#include <list>

using namespace std;

void main()
{
  list< int > list1;

  list1.push_back(10);
  list1.push_back(20);
  list1.push_back(30);
  list1.push_back(40);
  list1.push_back(50);
  
  list< int >::iterator iterPos = list1.begin();
  while( iterPos != list1.end() )
  {
    // 3으로 나누어지는 것은 제거한다.
    if( 0 == (*iterPos % 3) )
    {
      // 삭제 되는 것의 다음 반복자를 저장하고 또 이동하지 않게 한다.
      iterPos = list1.erase( iterPos );
      continue;
    }
    cout << "list1 : " << *iterPos << endl;
    ++iterPos;
  }
}

remove 

list에서 지정한 값과 일치하는 모든 데이터 삭제. erase와 다른 점은 erase는 반복자를 통해서 삭제하지만 remove는 값을 통해서 삭제합니다.

원형 : void remove( const Type& _Val );

list1에 담겨 있는 요소 중 특정 값과 일치하는 것을 모두 삭제하고 싶을 때는 아래와 같이 합니다.

// 20을 삭제한다.
list1.remove( 20 );

위에서는 값 삭제를 했지만 list가 구조체(클래스)의 포인터를 담고 있다면 삭제를 원하는 구조체의 포인터를 통해서 삭제가 가능합니다. 아래는 pitem2 구조체의 포인터를 삭제합니다.

// Item 포인터를 담아야한다.
list< Item* > Itemlist;

Item* pitem1 = new Item( 10, 100 );  Itemlist.push_back( pitem1 );
Item* pitem2 = new Item( 20, 200 );  Itemlist.push_back( pitem2 );
Item* pitem3 = new Item( 30, 300 );  Itemlist.push_back( pitem3 );

// pitem2를 삭제한다.
Itemlist.remove( pitem2 );

remove의 사용법에 대한 전체 코드입니다. 

#include <iostream>
#include <list>

using namespace std;

// 아이템 구조체
struct Item
{
  Item( int itemCd, int buyMoney )
  {
    ItemCd = itemCd;
    BuyMoney = buyMoney;
  }

  int ItemCd;  // 아이템 코드
  int BuyMoney;  // 판매 금액
};


void main()
{
  list< int > list1;

  list1.push_back(10);
  list1.push_back(20);
  list1.push_back(20);
  list1.push_back(30);

  list< int >::iterator iterEnd = list1.end();
  for(list< int >::iterator iterPos = list1.begin(); 
    iterPos != iterEnd; 
    ++iterPos )
  {
    cout << "list 1 : " << *iterPos << endl;
  }

  cout << endl << "remove  테스트 1" << endl;

  // 20을 삭제한다.
  list1.remove( 20 );

  iterEnd = list1.end();
  for(list< int >::iterator iterPos = list1.begin(); 
    iterPos != iterEnd; 
    ++iterPos )
  {
    cout << "list 1 : " << *iterPos << endl;
  }
  

  cout << endl << "remove  테스트 2 - 구조체를 삭제" << endl;

  // Item 포인터를 담아야한다.
  list< Item* > Itemlist;
  
  Item* pitem1 = new Item( 10, 100 );  Itemlist.push_back( pitem1 );
  Item* pitem2 = new Item( 20, 200 );  Itemlist.push_back( pitem2 );
  Item* pitem3 = new Item( 30, 300 );  Itemlist.push_back( pitem3 );
  
  // pitem2를 삭제한다.
  Itemlist.remove( pitem2 );
  
  list< Item* >::iterator iterEnd2 = Itemlist.end();
  for(list< Item* >::iterator iterPos = Itemlist.begin(); 
    iterPos != iterEnd2; 
    ++iterPos )
  {
    cout << "Itemlist : " << (*iterPos)->ItemCd << endl;
  }
}

결과 

결과4 

에서 구조체의 포인터를 담아서 삭제하는 것을 잘 보시기를 바랍니다. 보통 책에서는 이미 정의된 자료 타입만을 삭제하는 것을 보여주는데 사용자 정의 타입이라도 포인터로 담으면 해당 포인터로 삭제가 가능합니다. 

remove_if 

predicate을 만족하는 모든 데이터 삭제.
remove와 다른 점은 함수 객체를 사용하여 매개 변수로 전달된 인자를 조사하여 true라면 삭제하는 것입니다.
참고로 함수 객체라는 것은 괄호 연산자를 멤버함수로 가지는 클래스(또는 구조체) 객체입니다.
일반적으로 많이 사용되는 함수 객체는 STL에 정의 되어 있습니다.

원형 : template<class Predicate> void remove_if( Predicate _Pred );

remove_if에 사용할 함수 객체를 먼저 선언합니다.

// 20 이상 30 미만이면 true
template <typename T> class Is_Over20_Under30 : public std::unary_function 
{
public:
  bool operator( ) ( T& val ) 
  {
    return ( val >= 20 && val < 30 );
  }
};

list에서 remove_if에 함수 객체를 사용하여 list의 요소를 삭제하는 방법입니다.

  list< int > list1;

  list1.push_back(10);
  list1.push_back(20);
  list1.push_back(25);
  list1.push_back(30);
  list1.push_back(34);

  // 20 이상 30 미만은 삭제한다.
  list1.remove_if( Is_Over20_Under30< int >() );

list1의 요소 중 20 이상 30 미만은 모두 삭제합니다. 

아래는 remove_if 사용 예입니다. 

#include <iostream>
#include <list>

using namespace std;

// 20 이상 30 미만이면 true
template <typename T> class Is_Over20_Under30 : public std::unary_function 
{
public:
   bool operator( ) ( T& val ) 
   {
      return ( val >= 20 && val < 30 );
   }
};

void main()
{
  list< int > list1;

  list1.push_back(10);
  list1.push_back(20);
  list1.push_back(25);
  list1.push_back(30);
  list1.push_back(34);

  // 20 이상 30 미만은 삭제한다.
  list1.remove_if( Is_Over20_Under30< int >() );

  list< int >::iterator iterEnd = list1.end();
  for(list< int >::iterator iterPos = list1.begin(); 
    iterPos != iterEnd; 
    ++iterPos )
  {
    cout << "list 1 : " << *iterPos << endl;
  }
}

결과 

결과5 

sort 

데이터들을 정렬합니다. STL에 정의된 방식으로 정렬하거나 사용자가 정의한 방식으로 정렬할 수 있습니다.

원형 : template<class Traits> void sort( Traits _Comp );

sort 멤버를 사용하면 list1에 있는 요소들이 올림차순으로 정렬합니다.

// 올림 차순으로 정렬한다.
list1.sort();

내림차순으로 정렬한다면 greater를 사용합니다.

list1.sort( greater< int >() );

greater< int >는 greater< T > 라는 이미 정의되어 있는 함수 객체를 사용한 것입니다.
greater< int >는 int 형 x, y를 비교해서 x > y이면 true를 반환합니다.
그리고 greater외에 >=의 greater_equal, <=의 less_equal를 사용할 수 있습니다.
greater 이외의 것도 사용해 보기를 바랍니다.
사용자 정의 함수로 정렬하려면 함수 객체를 만들어야 합니다.
아래 함수 객체는 T의 멤버 중 ItemCd를 서로 비교하여 정렬을 합니다.

// 함수 객체 정의
template <typename T> struct COMPARE_ITEM
{
  bool operator()( const T l, const T r ) const
  {
    // 정렬 시에는 올림 차순으로된다. 내림 차순으로 하고 싶으면 < 에서 > 로
    // 변경하면 된다.
    return l.ItemCd < r.ItemCd;
  }
};

정의가 끝나면 아래와 같이 사용하면 됩니다.

Itemlist.sort( COMPARE_ITEM< Item >() );

Itemlist가 담고 있는 Item은 ItemCd를 기준으로 올림 차순으로 정렬한다.
아래 list의 sort 및 유저가 정의한 함수 객체를 사용한 sort에 대한 코드입니다. 

#include <iostream>
#include <list>

using namespace std;


// 함수 객체 정의
template <typename T> struct COMPARE_ITEM
{
    bool operator()( const T l, const T r ) const
    {
    // 정렬 시에는 올림 차순으로된다. 내림 차순으로 하고 싶으면 < 에서 > 로
    // 변경하면 된다.
      return l.ItemCd < r.ItemCd;
    }
};

void main()
{
  list< int > list1;

  list1.push_back(20);
  list1.push_back(10);
  list1.push_back(35);
  list1.push_back(15);
  list1.push_back(12);

  cout << "sort 올림차순" << endl;
  // 올림 차순으로 정렬한다.
  list1.sort();

  list< int >::iterator iterEnd = list1.end();
  for(list< int >::iterator iterPos = list1.begin(); 
    iterPos != iterEnd; 
    ++iterPos )
  {
    cout << "list 1 : " << *iterPos << endl;
  }

  cout << endl << "sort 내림차순" << endl;
  // 내림 차순으로 정렬한다.
  list1.sort( greater< int >() );

  iterEnd = list1.end();
  for(list< int >::iterator iterPos = list1.begin(); 
    iterPos != iterEnd; 
    ++iterPos )
  {
    cout << "list 1 : " << *iterPos << endl;
  }

  cout << endl << "sort - 유저가 정의한 방식으로 정렬" << endl;

  list< Item > Itemlist;
  
  Item item1( 20, 100 );  Itemlist.push_back( item1 );
  Item item2( 10, 200 );  Itemlist.push_back( item2 );
  Item item3( 7, 300 );  Itemlist.push_back( item3 );
  
  // 정렬한다.
  Itemlist.sort( COMPARE_ITEM< Item >() );
  
  list< Item >::iterator iterEnd2 = Itemlist.end();
  for(list< Item >::iterator iterPos = Itemlist.begin(); 
    iterPos != iterEnd2; 
    ++iterPos )
  {
    cout << "Itemlist : " << iterPos->ItemCd << endl;
  }
}

결과 

결과6 

보통 책에서는 list에서 제공하는 sort를 사용하는 설명이 일반적입니다. 그러나 현실에서는 유저정의 형의 데이터를 list에 담아서 사용하므로 유저가 정의한 함수 객체를 사용하여 정렬하는 경우가 많습니다.
이것으로 list에서 일반적으로 가장 자주 사용하는 멤버들에 대해서 알아 보았습니다.
아직 소개하지 않은 멤버들도 더 있습니다. 그러나 보통 현재까지 설명한 것들만 알고 있으면 list를 사용하는데 별 어려움이 없습니다. 소개하지 않은 멤버는 뒤에 표로 정리하겠습니다.
그럼 지금까지 배운 것을 토대로 list를 사용하여 이전 회에서 만들었던 스택을 개선해 보겠습니다.

list를 사용한 스택

이전 회에 설명한 template로 만들었던 스택에 대해서 잘 기억이 나지 않는 분들은 다시 한번 봐 주세요. 

http://network.hanb.co.kr/view.php?bi_id=1572 

이전 회에 만들었던 스택은 유연성이 부족합니다. 저장 공간의 크기가 고정적이고, LIFO(후입선출. 마지막에 들어간 것이 먼저 나온다) 방식으로만 작동합니다. 이것을 저장 공간의 크기가 가변적이고, FIFO(선입선출. 먼저 들어간 것이 먼저 나온다) 방식으로도 저장이 가능하도록 합니다. 

#include <iostream>
#include <list>

using namespace std;

template<typename T> 
class Stack
{
public:
  Stack() { Clear(); }

  // 저장 방식을 설정한다.
  void SetInOutType( bool bLIFO ) { m_bLIFO = bLIFO; }

  // 초기화 한다.
  void Clear()
  {
    if( false == m_Datas.empty() )
      m_Datas.clear();
  }

  // 스택에 저장된 개수
  int Count() { return static_cast( m_Datas.size() ); }

  // 저장된 데이터가 없는가?
  bool IsEmpty() { return m_Datas.empty(); }

  
  // 데이터를 저장한다.
  void push( T data )
  {
    m_Datas.push_back( data ); 
  }

  // 스택에서 빼낸다.
  bool pop( T* data )
  {
    if( IsEmpty() )
    {
      return false;
    }


    if( m_bLIFO )
    {
      memcpy( data, &m_Datas.back(), sizeof(T) );
      m_Datas.pop_back();
    }
    else
    {
      memcpy( data, &m_Datas.front(), sizeof(T) );
      m_Datas.pop_front();
    }

    return true;
  }

private:
  list m_Datas;
  bool  m_bLIFO; // true 이면 후입선출, false 이면 선입선출
};



void main()
{
  Stack< int > Int_Stack;

  // LIFO로 설정
  Int_Stack.SetInOutType( true );

  Int_Stack.push( 10 );
  Int_Stack.push( 20 );
  Int_Stack.push( 30 );

  int Value = 0;
  Int_Stack.pop( &Value );
  cout << "LIFO pop : " << Value << endl << endl;

  Int_Stack.Clear();


  // FIFO로 설정
  Int_Stack.SetInOutType( false );

  Int_Stack.push( 10 );
  Int_Stack.push( 20 );
  Int_Stack.push( 30 );

  Int_Stack.pop( &Value );
  cout << "FIFO pop : " << Value << endl << endl;
}

결과 

결과7 

List에 대해서 가장 많이 사용하는 것을 기준으로 설명했는데 그림과 예제 코드를 보면서 이해가 잘 되었는지 모르겠네요. 

보통 STL 관련 글을 보면 int나 float과 같은 기본 자료 타입을 사용하는 것에 대해서는 잘 나오지만, 유저 정의 자료 형을 사용하는 것에 대해서는 잘 나오지 않습니다. 그러니 예제 코드 중 유저 정의 자료 형을 사용한 부분을 특히 잘 보시기를 바랍니다. 다만, 유저 정의 자료 형을 사용하는 경우 함수 객체라는 것을 알아야 정확하게 이해가 될 테니 함수 객체 부분에 대해서는 아직 설명을 제대로 하지 않아서 잘 이해가 되지 않을까 걱정이 됩니다. 이 부분에 대해서는 다음에 함수 객체에 대해서 설명할 때 다시 언급 하겠습니다. 

list의 멤버는 제가 설명한 것 이외에도 더 있으니 좀 더 알고 싶은 분들은 http://msdn.microsoft.com/en-us/library/00k1x78a.aspx를 참고해 주세요. 

표3
표 3. 설명하지 않은 list의 멤버들

과제

이번 회부터는 STL 라이브러리를 설명하고 있으므로 배운 것을 활용하여 프로그램을 만들어 볼 수가 있습니다. 일방적으로 저의 글만 보는 것은 심심할 테니 제가 내는 문제를 풀어 보시기를 바랍니다.^^ 

과제 그림1
[과제 그림 1] 점 5개로 이루어진 도형 

위 그림은 순서대로 A, B, C, D, E 점을 찍은 후 서로 연결하여 도형이 만들어진 것입니다. 

과제 1) 이것을 list를 사용하여 만들어 보세요 

꼭 그림을 그리지 않아도 됩니다. A, B, C, D, E의 값을 순서대로 넣고 순서대로 출력하면 됩니다. 

과제 그림2
[과제 그림 2] 새로운 점 F 추가 

과제 2) 점 F가 새로 추가 되었습니다. A, B, C, D, F, E 순으로 선이 연결 되도록 해 보세요. 

과제 3) 점 D의 값을 (200, 100)으로 변경해 보세요. 

과제 4) 점 C를 삭제해 보세요. 

아주 간단하게 list 조작을 테스트 해 볼 수 있는 간단한 과제라고 생각합니다. 꼭 프로그램을 만들어 보세요.


출처 : http://www.hanb.co.kr/

Trackback 0 And Comment 0

[펌] About STL : C++ STL 프로그래밍(2-2)

|

제공: 한빛 네트워크
저자: 최흥배
이전기사:

이전 기사에서는 함수 템플릿에 대해 설명을 했으니 이번에는 클래스 템플릿에 대해서 설명하려고 합니다. 클래스 템플릿을 아주 간단하게 말하면 함수 템플릿이 함수에 템플릿을 사용한 것처럼 클래스 템플릿은 클래스에 템플릿을 사용한 것입니다. 

그러니 함수 템플릿에 대해서 잘 모르시는 분은 꼭 함수 템플릿에 대한 글을 먼저 보고 이 글을 보는 것이 이해하기에 좋습니다.

경험치 변경 이력 저장

기획팀에서 유저들이 게임에 접속하여 다른 유저들과 100번의 게임을 했을 때 유저들의 경험치가 변경 되는 이력을 볼 수 있기를 요청 하였습니다. 

기획팀의 요구를 들어주기 위해서 저는 게임이 끝날 때마다 경험치를 저장합니다. 또 경험치 이력 내역을 출력할 때 가장 최신에서 가장 오랜 된 것을 보여줘야 되기 때문에 스택(stack)이라는 자료 구조를 사용합니다.

스택은 자료 구조 중의 하나로 가장 마지막에 들어 온 것을 가장 먼저 꺼내는 LIFO(Last In First Out) 형식으로 되어 있습니다. 데이터를 넣을 때를 push, 빼낼 때는 pop이라는 이름을 일반적으로 사용한다.

경험치 이력을 저장하는 클래스의 구현과 이것을 사용하는 것은 아래와 같습니다. 

// 경험치를 저장할 수 있는 최대 개수
const int MAX_EXP_COUNT = 100;

// 경험치 저장 스택 클래스
class ExpStack
{
public:
  ExpStack()
  {
    Clear();
  }

  // 초기화 한다.
  void Clear()
  {
    m_Count = 0;
  }

  // 스택에 저장된 개수
  int Count()
  {
    return m_Count;
  }

        // 저장된 데이터가 없는가?
  bool IsEmpty()
  {
    return 0 == m_Count ? true : false;
  }

  // 경험치를 저장한다.
  bool push( float Exp )
  {
    // 저장할 수 있는 개수를 넘는지 조사한다.
    if( m_Count >= MAX_EXP_COUNT )
    {
      return false;
    }

    // 경험치를 저장 후 개수를 하나 늘린다.
    m_aData[ m_Count ] = Exp;
    ++m_Count;

    return true; 
  }

  // 스택에서 경험치를 빼낸다.
  float pop()
  {
    // 저장된 것이 없다면 0.0f를 반환한다.
    if( m_Count  < 1 )
    {
      return 0.0f;
    }

    // 개수를 하나 감소 후 반환한다.
    --m_Count;
    return m_aData[ m_Count ];
  }

private:
  float  m_aData[MAX_EXP_COUNT];
  int    m_Count;
};

#include 

using namespace std;

void main()
{
  ExpStack kExpStack;
  
  cout << "첫번째 게임 종료- 현재 경험치 145.5f" << endl;
  kExpStack.push( 145.5f );

  cout << "두번째 게임 종료- 현재 경험치 183.25f" << endl;
  kExpStack.push( 183.25f );

  cout << "세번째 게임 종료- 현재 경험치162.3f" << endl;
  kExpStack.push( 162.3f );


  int Count = kExpStack.Count();
  for( int i = 0; i < Count; ++i )
  {
    cout << "현재 경험치->" << kExpStack.pop() << endl;
  }
}

실행 결과 

그림1 

실행 결과를 보면 알 수 있듯이 스택 자료구조를 사용하였기 때문에 제일 뒤에 넣은 데이터가 가장 제일 먼저 출력 되었습니다.

게임 돈 변경 이력도 저장해 주세요

경험치 변경 이력을 저장하고 출력하는 기능을 만들어서 기획팀에 보여주니 이번에는 게임 돈의변경 이력도 보고 싶다고 말합니다. 

위에서 경험치 변경 이력 저장 기능을 만들어 보았으니 금방 할 수 있는 것이죠. 그래서 이번에는 이전 보다 훨씬 더 빨리 만들었습니다. 

// 돈을 저장할 수 있는 최대 개수
const int MAX_MONEY_COUNT = 100;

// 돈 저장 스택 클래스
class MoneyStack
{
public:
  MoneyStack()
  {
    Clear();
  }

  // 초기화 한다.
  void Clear()
  {
    m_Count = 0;
  }

  // 스택에 저장된 개수
  int Count()
  {
    return m_Count;
  }

  // 저장된 데이터가없는가?
  bool IsEmpty()
  {
    return 0 == m_Count ? true : false;
  }

  // 돈을 저장한다.
  bool push( __int64 Money )
  {
    // 저장 할 수 있는 개수를 넘는지 조사한다.
    if( m_Count >= MAX_MONEY_COUNT )
    {
      return false;
    }

    // 저장후 개수를 하나 늘린다.
    m_aData[ m_Count ] = Money;
    ++m_Count;

    return true; 
  }

  // 스택에서 돈을 빼낸다.
  __int64 pop()
  {
    // 저장된 것이 없다면 0을 반환한다.
    if( m_Count  < 1 )
    {
      return 0;
    }

    // 개수를 하나 감소 후 반환한다.
    --m_Count;
    return m_aData[ m_Count ];
  }

private:
  __int64  m_aData[MAX_MONEY_COUNT];
  int  m_Count;
};

ExpStack 클래스와 MoneyStack 클래스가 비슷합니다 

게임 돈 변경 이력 저장 기능을 가지고 있는 MoneyStack 클래스를 만들고 보니 앞에 만든 ExpStack와 거의 같습니다. 저장하는 데이터의 자료형만 다를뿐이지 모든 것이 같습니다. 그리고 기획팀에서는 게임 캐릭터의 Level 변경 이력도 저장하여 보여주기를 바라는 것 같습니다. 이미 거의 똑같은 클래스를 두개 만들었고 앞으로도 기획팀에서 요청이 있으면 더 만들 것 같습니다. 이렇게 자료형만 다른 클래스를 어떻게 하면 하나의 클래스로 정의 할수 있을까요? 이와 비슷한 문제를 이전의 "함수 템플릿"에서도 나타나지 않았나요? 그때 어떻게 해결했죠?(생각나지 않는 분들은 앞의 "함수 템플릿"을 다시 한번 봐 주세요 ^^) 

템플릿으로 하면됩니다. 

기능은 같지만 변수의 자료형만 다른 함수를 템플릿을 사용하여 하나의 함수로 정의했듯이 이번에는 템플릿을 사용하여 클래스를 정의합니다. 클래스에서 템플릿을 사용하면 이것을 클래스 템플릿이라고 합니다. 클래스 템플릿을 사용하면 위에서 중복된 클래스를 하나의 클래스로 만들 수 있습니다.

클래스 템플릿을 사용하는 방법

클래스 템플릿을 정의하는 문법은 아래와 같습니다. 

그림2 

정의한 클래스 템플릿을 사용하는 방법은 아래와 같습니다. 

그림3

Stack 템플릿 클래스

지금까지 만들었던 ExpStack 과 MoneyStack을 클래스 템플릿으로 만든 코드는 아래와 같습니다. 

const int MAX_COUNT = 100;

template<typename T> 
class Stack
{
public:
  Stack()
  {
    Clear();
  }

  // 초기화 한다.
  void Clear()
  {
    m_Count = 0;
  }

  // 스택에 저장된 개수
  int Count()
  {
    return m_Count;
  }

  // 저장된 데이터가 없는가?
  bool IsEmpty()
  {
    return 0 == m_Count ? true : false;
  }

  // 데이터를 저장한다.
  bool push( T data )
  {
    // 저장 할수 있는 개수를 넘는지 조사한다.
    if( m_Count >= MAX_COUNT )
    {
      return false;
    }

    // 저장후 개수를 하나 늘린다.
    m_aData[ m_Count ] = data;
    ++m_Count;

    return true; 
  }

  // 스택에서 빼낸다.
  T pop()
  {
    // 저장된 것이 없다면 0을 반환한다.
    if( m_Count  < 1 )
    {
      return 0;
    }

    // 개수를 하나 감소 후 반환한다.
    --m_Count;
    return m_aData[ m_Count ];
  }

private:
  T  m_aData[MAX_COUNT];
  int    m_Count;
};

#include 

using namespace std;

void main()
{
  Stack kStackExp;
  
  cout << "첫번째 게임 종료- 현재 경험치 145.5f" << endl;
  kStackExp.push( 145.5f );

  cout << "두번째 게임 종료- 현재 경험치 183.25f" << endl;
  kStackExp.push( 183.25f );

  cout << "세번째 게임 종료- 현재 경험치 162.3f" << endl;
  kStackExp.push( 162.3f );


  int Count = kStackExp.Count();
  for( int i = 0; i < Count; ++i )
  {
    cout << "현재 경험치->" << kStackExp.pop() << endl;
  }

  cout << endl << endl;

  Stack<__int64> kStackMoney;
  
  cout << "첫번째 게임 종료- 현재 돈 1000023" << endl;
  kStackMoney.push( 1000023 );

  cout << "두번째 게임 종료- 현재 돈 1000234" << endl;
  kStackMoney.push( 1000234 );

  cout << "세번째 게임 종료- 현재 돈 1000145" << endl;
  kStackMoney.push( 1000145 );


  Count = kStackMoney.Count();
  for( int i = 0; i < Count; ++i )
  {
    cout << "현재 돈->" << kStackMoney.pop() << endl;
  }
}

실행 결과 

그림4 

클래스 템플릿으로 Stack을 구현하여 앞으로 다양한 데이터를 사용할 수 있게 되었습니다. 

그런데 위의 Stack 클래스는 부족한 부분이 있습니다. 앞으로 이 부족한 부분을 채워 나가면서 클래스 템플릿에 대해서 좀 더 알아 보겠습니다.

클래스 템플릿에서 non-type 파라메터 사용

위에서 만든 Stack 클래스는 데이터를 저장할 수 있는 공간이 100개로 정해져 있습니다. Stack의 크기는 사용하는 곳에 따라서 변동될 수 있어야 사용하기에 적합합니다. 

함수 템플릿을 설명할 때도 non-type이 나왔는데 사용 방법이 거의 같습니다. 템플릿 파라메터를 기본 데이터 형으로 합니다. 아래의 사용 예를 보시면 금방 이해가 갈 것입니다. 

// 템플릿 파라메터중 int Size가 non-type  파라메터입니다.
template<typename T, int Size> 
class Stack
{
public:
  Stack()
  {
    Clear();
  }

  // 초기화 한다.
  void Clear()
  {
    m_Count = 0;
  }

  // 스택에 저장된 개수
  int Count()
  {
    return m_Count;
  }

  // 저장된 데이터가 없는가?
  bool IsEmpty()
  {
    return 0 == m_Count ? true : false;
  }

  // 데이터를 담을수 있는 최대 개수
  int GetStackSize()
  {
    return Size;
  }

  // 데이터를 저장한다.
  bool push( T data )
  {
    // 저장할 수 있는 개수를 넘는지 조사한다.
    if( m_Count >= Size )
    {
      return false;
    }

    // 저장 후 개수를 하나 늘린다.
    m_aData[ m_Count ] = data;
    ++m_Count;

    return true; 
  }

  // 스택에서 빼낸다.
  T pop()
  {
    // 저장된 것이 없다면 0을 반환한다.
    if( m_Count  < 1 )
    {
      return 0;
    }

    // 개수를 하나 감소 후 반환한다.
    --m_Count;
    return m_aData[ m_Count ];
  }

private:
  T  m_aData[Size];
  int  m_Count;
};

#include 

using namespace std;

void main()
{
  Stack kStack1;
  cout << "스택의 크기는?" << kStack1.GetStackSize() << endl;

  Stack kStack2;
  cout << "스택의 크기는?" << kStack2.GetStackSize() << endl;
}

실행 결과 

그림5

템플릿 파라메터 디폴트 값 사용

일반 함수에서 함수 인자의 디폴트 값을 지정하듯이 클래스 템플릿의 파라메터도 디폴트 값으로 할 수 있습니다. 

// 템플릿 파라메터중 int Size가 non-type 파라메터입니다. 
// Size의 디폴트 값을 100으로 합니다.
template<typename T, int Size=100> 
class Stack
{
   …..  생략
}

void main()
{
  Stack kStack1;
  cout << "스택의크기는?" << kStack1.GetStackSize() << endl;

  Stack kStack2;
  cout << "스택의크기는?" << kStack2.GetStackSize() << endl;
}

List5에서 템플릿 파라메터 중 Size의 값을 디폴트 100으로 했습니다. 클래스를 생성할 때 두 번째 파라메터 값을 지정하지 않으면 디폴트 값이 사용 됩니다. 

실행 결과 

그림6

스택 클래스의 크기를 클래스 생성자에서 지정

클래스 템플릿에 대한 설명을 계속 하기 위해 현재까지 만든 스택 클래스를 변경합니다. 스택의 크기를 클래스 템플릿 파라메터가 아닌 생성자에서 지정하도록 변경하겠습니다. 

template<typename T, int Size=100> 
class Stack
{
public:
  explicit Stack( int size )
  {
    m_Size = size;
    m_aData = new T[m_Size];

    Clear();
  }

  ~Stack()
  {
    delete[] m_aData;
  }

  // 초기화 한다.
  void Clear()
  {
    m_Count = 0;
  }

  // 스택에 저장된 개수
  int Count()
  {
    return m_Count;
  }

  // 저장된 데이터가 없는가?
  bool IsEmpty()
  {
    return 0 == m_Count ? true : false;
  }

  // 데이터를 담을 수 있는 최대 개수
  int GetStackSize()
  {
    return m_Size;
  }

  // 데이터를 저장한다.
  bool push( T data )
  {
    // 저장할 수 있는 개수를 넘는지 조사한다.
    if( m_Count >= m_Size )
    {
      return false;
    }

    // 저장 후 개수를 하나 늘린다.
    m_aData[ m_Count ] = data;
    ++m_Count;

    return true; 
  }

  // 스택에서 빼낸다.
  T pop()
  {
    // 저장된 것이 없다면 0을 반환한다.
    if( m_Count  < 1 )
    {
      return 0;
    }

    // 개수를 하나 감소 후 반환한다.
    --m_Count;
    return m_aData[ m_Count ];
  }

private:
  T*  m_aData;
  int  m_Count;

  int m_Size;
};

#include 

using namespace std;


void main()
{
  Stack kStack1(64);
  cout << "스택의 크기는? " << kStack1.GetStackSize() << endl;
}

실행결과 

그림7 

List 6의 코드에서 잘 보지 못한 키워드가 있을 것입니다. 바로 explicit 입니다. explicit 키워드로 규정된 생성자는 암시적인 형 변환을 할 수 없습니다. 그래서 List6의 void main()에서

  Stack kStack1 = 64; 

로 클래스를 생성하면 컴파일 에러가 발생합니다.

클래스 템플릿 전문화

기획팀에서 새로운 요구가 들어왔습니다. 이번에는 게임을 할 때 같이 게임을 했던 유저의 아이디를 저장하여 보여주기를 원합니다. 지금까지 만든 Stack 클래스는 기본 자료형을 사용하는 것을 전제로 했는데 유저의 아이디를 저장하려면 문자열이 저장되어야 하므로 사용할 수가 없습니다. 

기본 자료형으로 하지 않고 문자열을 사용한다는 것만 다르지 작동은 비슷하므로 Stack이라는 이름의 클래스를 사용하고 싶습니다. 기존의 Stack 클래스 템플릿과 클래스의 이름만 같지 행동은 다른 Stack 클래스를 구현 하려고 합니다. 이때 필요한 것인 클래스 템플릿의 전문화라는 것입니다. 클래스 템플릿 전문화는 기존에 구현한 클래스 템플릿과 이름과 파라메터 개수는 같지만 파라메터를 특정한 것으로 지정합니다. 

전문화된 클래스 템플릿 정의는 다음과 같은 형태를 가진다.

template <> 
class 클래스 이름<지정된 타입>
{
   ……………….
};

아래의 코드는 문자열을 저장하기 위해 char* 으로 전문화한 Stack 클래스입니다. 

// ID 문자열의 최대 길이(null 문자포함)
const int MAX_ID_LENGTH = 21;

// char* 를 사용한 Stack 클래스(List 6) 템플릿 전문화
template<> 
class Stack
{
public:
  explicit Stack( int size )
  {
    m_Size = size;

    m_ppData = new char *[m_Size];
    for( int i = 0; i < m_Size; ++i )
    {
      m_ppData[i] = new char[MAX_ID_LENGTH];
    }

    Clear();
  }

  ~Stack()
  {
    for( int i = 0; i < m_Size; ++i )
    {
      delete[] m_ppData[i];
    }

    delete[] m_ppData;
  }

  // 초기화한다.
  void Clear()
  {
    m_Count = 0;
  }

  // 스택에 저장된 개수
  int Count()
  {
    return m_Count;
  }

  // 저장된 데이터가 없는가?
  bool IsEmpty()
  {
    return 0 == m_Count ? true : false;
  }

  // 데이터를 담을 수 있는 최대 개수
  int GetStackSize()
  {
    return m_Size;
  }

  // 데이터를 저장한다.
  bool push( char* pID )
  {
    // 저장할 수 있는 개수를 넘는지 조사한다.
    if( m_Count >= m_Size )
    {
      return false;
    }

    // 저장 후 개수를 하나 늘린다.
    strncpy_s( m_ppData[m_Count], MAX_ID_LENGTH, pID, MAX_ID_LENGTH - 1);
    m_ppData[m_Count][MAX_ID_LENGTH - 1] = '\0';

    ++m_Count;

    return true; 
  }

  // 스택에서 빼낸다.
  char* pop()
  {
    // 저장된 것이 없다면 0을 반환한다.
    if( m_Count  < 1 )
    {
      return 0;
    }

    // 개수를 하나 감소 후 반환한다.
    --m_Count;
    return m_ppData[ m_Count ];
  }

private:
  char** m_ppData;
  int  m_Count;

  int m_Size;
};

#include 

using namespace std;

void main()
{
  Stack kStack1(64);
  cout << "스택의 크기는? " << kStack1.GetStackSize() << endl;
  kStack1.push( 10 );
  kStack1.push( 11 );
  kStack1.push( 12 );

  int Count1 = kStack1.Count();
  for( int i = 0; i < Count1; ++i )
  {
    cout << "유저의 레벨 변화 -> " << kStack1.pop() << endl;
  }

  cout << endl;

  char GameID1[MAX_ID_LENGTH] = "NiceChoi";
  char GameID2[MAX_ID_LENGTH] = "SuperMan";
  char GameID3[MAX_ID_LENGTH] = "Attom";

  // Stack 클래스 템플릿의 char*  전문화 버전을 생성한다.
  Stack kStack2(64);
  kStack2.push(GameID1);
  kStack2.push(GameID2);
  kStack2.push(GameID3);

  int Count2 = kStack2.Count();
  for(int i = 0; i < Count2; ++i)
  {
    cout << "같이 게임을 한유저의 ID -> " << kStack2.pop() << endl;
  }
}

실행 결과 

그림8

클래스 템플릿 부분 전문화

클래스 템플릿은 템플릿 파라메터 중 일부를 구체적인 형(type)을 사용, 또는 템플릿 파라메터를 포인터나 참조를 사용하여 부분 전문화를 할 수 있습니다. 

- 구체적인 형 사용에 의한 부분 전문화

template< typename T1, typename T2 > class Test { …. };

의 T2를 float로 구체화 하여 부분 전문화를 하면 다음과 같습니다.

template< typename T1 > class Test { ….. };

코드는 다음과 같습니다. 

template< typename T1, typename T2 >
class Test
{
public:
  T1 Add( T1 a, T2 b )
  {
    cout << "일반 템플릿을 사용했습니다." << endl;
    return a;
  }
};

// T2를 float로 구체화한 Test의 부분 전문화 템플릿
template< typename T1 > 
class Test
{
public:
  T1 Add( T1 a, float b )
  {
    cout << "부분 전문화 템플릿을 사용했습니다." << endl;
    return a;
  }
};

#include 

using namespace std;

void main()
{
  Test test1;
  test1.Add( 2, 3 );

  Test test2;
  test2.Add( 2, 5.8f );
}

그림9 

위의 예에서는 템플릿 파라메터 2개 중 일부를 구체화하여 부분 전문화를 했지만 당연하지만 2개 이상도 가능합니다.

template< typename T1, typename T2, typename T3 > class Test { …. };

의 부분 전문화 템플릿은

template< typename T1, typename T2 > class Test { ….. };

- 포인터의 부분 전문화

template< typename T > class TestP { …. };

의 T의 T* 부분 전문화를 하는 다음과 같습니다.

template< typename T > class TestP {  …… };

코드는 다음과 같습니다. 

template< typename T > 
class TestP
{
public:
  void Add()
  {
    cout << "일반 템플릿을 사용했습니다." << endl;
  }
};

// T를 T*로 부분 전문화
template< typename T > 
class TestP
{
public:
  void Add()
  {
    cout << "포인터를 사용한 부분 전문화 템플릿을 사용했습니다." << endl;
  }
};

#include 

using namespace std;

void main()
{
  TestP test1;
  test1.Add();

  TestP test2;
  test2.Add();
}

실행 결과 

그림10

싱글톤 템플릿 클래스

클래스 상속을 할 때 템플릿 클래스를 상속 받음으로 상속 받는 클래스의 기능을 확장할 수 있습니다. 

저의 경우 현업에서 클래스 템플릿을 가장 많이 사용하는 경우가 클래스 템플릿을 사용한 싱글톤 클래스 템플릿을 사용하는 것입니다. 

어떠한 객체가 꼭 하나만 있어야 되는 경우 싱글톤으로 정의한 클래스 템플릿을 상속 받도록 합니다.

싱글톤은 싱글톤 패턴을 말하는 것으로 어떤 클래스의 인스턴스가 꼭 하나만 생성되도록 하며, 전역적인 접근이 가능하도록 합니다. 어떤 클래스를 전역으로 사용하는 경우 복수개의 인스턴스가 생성되지 않도록 싱글톤 패턴으로 생성하는 것을 권장합니다.

사용하는 방법은 베이스 클래스를 템플릿을 사용하여 만듭니다. 그리고 이것을 상속 받는 클래스에서 베이스 클래스의 템플릿 파라메터에 해당 클래스를 사용합니다. 즉 싱글톤 클래스 템플릿은 이것을 상속 받는 클래스를 싱글톤으로 만들어줍니다. 

위에서 설명한 클래스 템플릿에 대하여 이해를 하셨다면 의 코드를 보면 이해를 할 수 있으리라 생각합니다. 싱글톤 클래스 템플릿은 직접 생성을 하지 않으므로 주 멤버들을 static로 만들어줍니다. 그리고 생성자를 통해서 _Singleton를 생성하지 않고 GetSingleton()을 통해서만 생성하도록 합니다. 

#include 
using namespace std;


// 파라메터 T를 싱글톤이 되도록 정의 합니다.
template <typename T>
class MySingleton
{
public:
    MySingleton() {}
    virtual ~MySingleton() {}

    // 이 멤버를 통해서만 생성이 가능합니다.
    static T* GetSingleton()
    {
        // 아직 생성이 되어 있지 않으면 생성한다.
        if( NULL == _Singleton ) {
            _Singleton = new T; 
        }

       return ( _Singleton );
    }

    static void Release()
    {
        delete _Singleton;
        _Singleton = NULL;
    }

private:
    static T* _Singleton;
};

template <typename T> T* MySingleton ::_Singleton = NULL;

// 싱글톤 클래스 템플릿을 상속 받으면서 파라메터에 본 클래스를 넘깁니다.
class MyObject : public MySingleton
{
public:
MyObject() : _nValue(10) {}

void SetValue( int Value ) { _nValue = Value;}  
int GetValue() { return _nValue; }

private :
int _nValue;
};

void main()
{
   MyObject* MyObj1 = MyObject::GetSingleton();

   cout << MyObj1->GetValue() << endl;

   // MyObj2는 Myobj1과 동일한 객체입니다.
   MyObject* MyObj2 = MyObject::GetSingleton();
   MyObj2->SetValue(20);

   cout << MyObj1->GetValue() << endl;
   cout << MyObj2->GetValue() << endl;
}

클래스 템플릿 코딩 스타일 개선

위에서 예제로 구현한 다양한 클래스 템플릿의 코딩 스타일은 클래스 선언 안에서 각 멤버들의 정의를 구현하고 있습니다. 클래스의 코드 길이가 크지 않은 경우는 코드를 보는데 불편하지 않지만 코드 길이가 길어지는 경우 클래스의 전체적인 윤곽을 바로 알아보기가 쉽지 않습니다. 

긴 코드를 가지는 클래스 템플릿의 경우는 클래스의 선언과 정의를 분리하는 것이 좋습니다. 위에서 예제로 나온 클래스 템플릿 중 의 Stack 클래스 템플릿을 선언과 정의를 분리하면 아래와 같습니다. 

template<typename T> 
class Stack
{
public:
  explicit Stack( int size );

  ~Stack();

  // 초기화 한다.
  void Clear();

  // 스택에 저장된 개수
  int Count();

  // 저장된 데이터가 없는가?
  bool IsEmpty();

  // 데이터를 담을 수 있는 최대 개수
  int GetStackSize();

  // 데이터를 저장한다.
  bool push( T data );

  // 스택에서 빼낸다.
  T pop();

private:
  T*  m_aData;
  int  m_Count;

  int m_Size;
};

template < typename T > 
Stack::Stack( int size )
{
  m_Size = size;
  m_aData = new T[m_Size];

  Clear();
}

template < typename T > 
Stack::~Stack()
{
  delete[] m_aData;
}

template < typename T > 
void Stack::Clear()
{
  m_Count = 0;
}

template < typename T > 
int Stack::Count()
{
  return m_Count;
}

template < typename T >
bool Stack::IsEmpty()
{
  return 0 == m_Count ? true : false;
}

template < typename T > 
int Stack::GetStackSize()
{
  return m_Size;
}

template < typename T > 
bool Stack::push( T data )
{
  // 저장할 수 있는 개수를 넘는지 조사한다.
  if( m_Count >= m_Size )
  {
    return false;
  }

  // 저장 후 개수를 하나 늘린다.
  m_aData[ m_Count ] = data;
  ++m_Count;

  return true; 
}

template < typename T > 
T Stack::pop()
{
  // 저장된 것이 없다면 0을 반환한다.
  if( m_Count  < 1 )
  {
    return 0;
  }

  // 개수를 하나 감소 후 반환한다.
  --m_Count;
  return m_aData[ m_Count ];
}

의 코드를 보면 알듯이 클래스 안에 정의를 했던 것과의 차이점은 클래스 멤버 정의를 할 때 템플릿 선언하고 클래스 이름에 템플릿 파라메터를 적어 줍니다.

클래스 선언과 정의를 각각 다른 파일에 하려면

일반적인 클래스의 경우 크기가 작은 경우를 제외하면 클래스의 선언과 정의를 서로 다른 파일에 합니다. 

클래스 템플릿의 경우는 일반적인 방법으로는 그렇게 할 수가 없습니다. 클래스 멤버 정의를 선언과 다른 파일에 하려면 멤버 정의를 할 때 'export'라는 키워드를 사용합니다. 의 GetStackSize()에 export를 사용하면 아래와 같이 됩니다.

template < typename T > 
export int Stack::GetStackSize()
{
  return m_Size;
}

그러나 export라는 키워드를 사용하면 컴파일 에러가 발생합니다. 이유는 현재 대부분의 C++ 컴파일러에서는 export라는 키워드를 지원하지 않습니다. export를 아직 지원하지 못하는 이유는 이것을 지원하기 위해 필요로 하는 노력은 컴파일러를을 새로 만들 정도의 노력을 필요로 할 정도로 어렵다고 합니다. 현재까지도 대부분의 컴파일러 개발자들은 구현 계획을 세우지도 않고 있으며 일부에서는 구현에 반대하는 의견도 있다고 합니다. 

그럼 클래스 템플릿의 선언과 정의를 서로 다른 파일에 할 수 있는 방법은 없을까요? 약간 편법을 사용하면 가능합니다. 

inline이라는 의미를 가지고 있는 '.inl' 확장자 파일에 클래스 구현하고 이 .inl 파일을 헤더 파일에서 포함합니다. (참고로 .inl 파일을 사용하는 것은 일반적인 방식은 아니고 일부 라이브러리나 상용 3D 엔진에서 간혹 사용하는 것을 볼 수 있습니다). 

의 Stack 클래스 템플릿의 선언과 정의를 다른 파일로 하는 예의 일부를 아래에 보여드리겠습니다.

// stack.h 파일
template<typename T> 
class Stack
{
public:

  // 초기화 한다.
  void Clear();
 
};

#include "stack.inl"

// stack.inl 파일

template < typename T > 
void Stack::Clear()
{
  m_Count = 0;
}

이것으로 클래스 템플릿에 대한 설명은 다 한 것 같습니다. 함수 템플릿에 대한 글을 이미 보셨으면 템플릿에 대한 어느 정도 이해를 가지고 있을 테니 어렵지 않게 이해를 할 수 있으리라 생각합니다만 저의 부족한 글 때문에 어렵지 않았을까라는 걱정도 조금합니다. 

글을 그냥 보고 넘기지 마시고 직접 코딩을 해 보시기를 권장합니다. 본문에 나오는 예제들은 모두 코드 길이가 짧은 것이라서 직접 코딩을 하더라도 긴 시간은 걸리지 않을 것입니다. 

다음회부터는 본격적으로 STL에 대한 설명에 들어갑니다. 전 회에서 이야기 했듯이 STL은 템플릿으로 만들어진 것입니다. 아직 템플릿의 유용성을 느끼지 못한 분들은 STL에 대해서 알게 되시면 템플릿의 뛰어남을 알게 되리라 생각합니다.


출처 : http://www.hanb.co.kr/

Trackback 0 And Comment 0