2013. 10. 3. 20:57ㆍC++
http://www.hanb.co.kr/network/view.html?bi_id=1572
경험치 변경 이력 저장
기획팀에서 유저들이 게임에 접속하여 다른 유저들과 100번의 게임을 했을 때 유저들의 경험치가 변경 되는 이력을 볼 수 있기를 요청 하였습니다.
기획팀의 요구를 들어주기 위해서 저는 게임이 끝날 때마다 경험치를 저장합니다. 또 경험치 이력 내역을 출력할 때 가장 최신에서 가장 오랜 된 것을 보여줘야 되기 때문에 스택(stack)이라는 자료 구조를 사용합니다.
경험치 이력을 저장하는 클래스의 구현과 이것을 사용하는 것은 아래와 같습니다.
// 경험치를 저장할 수 있는 최대 개수 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; }; #includeusing 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; } }
실행 결과를 보면 알 수 있듯이 스택 자료구조를 사용하였기 때문에 제일 뒤에 넣은 데이터가 가장 제일 먼저 출력 되었습니다.
게임 돈 변경 이력도 저장해 주세요
위에서 경험치 변경 이력 저장 기능을 만들어 보았으니 금방 할 수 있는 것이죠. 그래서 이번에는 이전 보다 훨씬 더 빨리 만들었습니다.
// 돈을 저장할 수 있는 최대 개수 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; };
게임 돈 변경 이력 저장 기능을 가지고 있는 MoneyStack 클래스를 만들고 보니 앞에 만든 ExpStack와 거의 같습니다. 저장하는 데이터의 자료형만 다를뿐이지 모든 것이 같습니다. 그리고 기획팀에서는 게임 캐릭터의 Level 변경 이력도 저장하여 보여주기를 바라는 것 같습니다. 이미 거의 똑같은 클래스를 두개 만들었고 앞으로도 기획팀에서 요청이 있으면 더 만들 것 같습니다. 이렇게 자료형만 다른 클래스를 어떻게 하면 하나의 클래스로 정의 할수 있을까요? 이와 비슷한 문제를 이전의 "함수 템플릿"에서도 나타나지 않았나요? 그때 어떻게 해결했죠?(생각나지 않는 분들은 앞의 "함수 템플릿"을 다시 한번 봐 주세요 ^^)
템플릿으로 하면됩니다.
기능은 같지만 변수의 자료형만 다른 함수를 템플릿을 사용하여 하나의 함수로 정의했듯이 이번에는 템플릿을 사용하여 클래스를 정의합니다. 클래스에서 템플릿을 사용하면 이것을 클래스 템플릿이라고 합니다. 클래스 템플릿을 사용하면 위에서 중복된 클래스를 하나의 클래스로 만들 수 있습니다.
클래스 템플릿을 사용하는 방법
정의한 클래스 템플릿을 사용하는 방법은 아래와 같습니다.
Stack 템플릿 클래스
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; }; #includeusing 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; } }
클래스 템플릿으로 Stack을 구현하여 앞으로 다양한 데이터를 사용할 수 있게 되었습니다.
그런데 위의 Stack 클래스는 부족한 부분이 있습니다. 앞으로 이 부족한 부분을 채워 나가면서 클래스 템플릿에 대해서 좀 더 알아 보겠습니다.
클래스 템플릿에서 non-type 파라메터 사용
함수 템플릿을 설명할 때도 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; }; #includeusing namespace std; void main() { Stack kStack1; cout << "스택의 크기는?" << kStack1.GetStackSize() << endl; Stack kStack2; cout << "스택의 크기는?" << kStack2.GetStackSize() << endl; }
템플릿 파라메터 디폴트 값 사용
// 템플릿 파라메터중 int Size가 non-type 파라메터입니다. // Size의 디폴트 값을 100으로 합니다. template<typename T, int Size=100> class Stack { ….. 생략 } void main() { StackkStack1; cout << "스택의크기는?" << kStack1.GetStackSize() << endl; Stack kStack2; cout << "스택의크기는?" << kStack2.GetStackSize() << endl; }
실행 결과
스택 클래스의 크기를 클래스 생성자에서 지정
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; }; #includeusing namespace std; void main() { Stack kStack1(64); cout << "스택의 크기는? " << kStack1.GetStackSize() << endl; }
List 6의 코드에서 잘 보지 못한 키워드가 있을 것입니다. 바로 explicit 입니다. explicit 키워드로 규정된 생성자는 암시적인 형 변환을 할 수 없습니다. 그래서 List6의 void main()에서
StackkStack1 = 64;
클래스 템플릿 전문화
기본 자료형으로 하지 않고 문자열을 사용한다는 것만 다르지 작동은 비슷하므로 Stack이라는 이름의 클래스를 사용하고 싶습니다. 기존의 Stack 클래스 템플릿과 클래스의 이름만 같지 행동은 다른 Stack 클래스를 구현 하려고 합니다. 이때 필요한 것인 클래스 템플릿의 전문화라는 것입니다. 클래스 템플릿 전문화는 기존에 구현한 클래스 템플릿과 이름과 파라메터 개수는 같지만 파라메터를 특정한 것으로 지정합니다.
전문화된 클래스 템플릿 정의는 다음과 같은 형태를 가진다.
template <> class 클래스 이름<지정된 타입> { ………………. };
// 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; } }
클래스 템플릿 부분 전문화
- 구체적인 형 사용에 의한 부분 전문화
template< typename T1, typename T2 > class Test { …. };
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 ); }
위의 예에서는 템플릿 파라메터 2개 중 일부를 구체화하여 부분 전문화를 했지만 당연하지만 2개 이상도 가능합니다.
template< typename T1, typename T2, typename T3 > class Test { …. };
template< typename T1, typename T2 > class Test{ ….. };
template< typename T > class TestP { …. };
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(); }
싱글톤 템플릿 클래스
저의 경우 현업에서 클래스 템플릿을 가장 많이 사용하는 경우가 클래스 템플릿을 사용한 싱글톤 클래스 템플릿을 사용하는 것입니다.
어떠한 객체가 꼭 하나만 있어야 되는 경우 싱글톤으로 정의한 클래스 템플릿을 상속 받도록 합니다.
위에서 설명한 클래스 템플릿에 대하여 이해를 하셨다면
#includeusing 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; }
그럼 클래스 템플릿의 선언과 정의를 서로 다른 파일에 할 수 있는 방법은 없을까요? 약간 편법을 사용하면 가능합니다.
inline이라는 의미를 가지고 있는 '.inl' 확장자 파일에 클래스 구현하고 이 .inl 파일을 헤더 파일에서 포함합니다. (참고로 .inl 파일을 사용하는 것은 일반적인 방식은 아니고 일부 라이브러리나 상용 3D 엔진에서 간혹 사용하는 것을 볼 수 있습니다).
// 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에 대해서 알게 되시면 템플릿의 뛰어남을 알게 되리라 생각합니다.