반응형


Feedback : smile.k06@gmail.com


그냥 코딩하다가 오류나고 생각나서 끄적끄적..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
 
template <typename T>
class testClass 
{
    // class 내부함수
public:   
      void testFunc();   
      void testFunc2();
    
    // 생성자, 파괴자
public:
      testClass();
      ~testClass();
};
 
// class 내부함수
void testClass::testFunc() {};   // C2955 에러
            
template <typename T>
void testClass<T>::testFunc2 {};    // 정상 컴파일
 
// 생성자
testClass::testClass();     //C2955 에러
 
template<typename T>
testClass<T>::testClass {};    //정상 컴파일
 
// 파괴자
testClass::~testClass();     //C2955 에러
 
template<typename T>
testClass<T>::~testClass {};    //정상 컴파일
 
 

testClass 를 template 화 시켰다.

비쥬얼 스튜디오에서 클래스 생성기로 클래스를 생성하면 생성자와 파괴자가 cpp파일에 자동 생성.
그러나 헤더파일에서 클래스를 템플릿 시켜주고 컴파일하면 그냥 오류가 남.

이거이가 무슨말이고 한가 해서.. 테스트를 해 보었다.


template 클래스는 컴파일이 됨과 동시에 타입을 생성해낸다. 입력과 동시에 타입생성이 아니라는 것.
외부에 함수를 정의할때 따라서 문제가 생긴다.
외부에 단순히 scope로 함수를 인식하게 해준다면,
컴파일은
"얘 타입 없음"
이라고 오류를 뱉는것임.

따라서 템플릿을 꼭 붙여주도록 해야겠다.

안뇽~





반응형
반응형

Feedback : smile.k06@gmail.com


갑자기 새벽에 자다 깨서 그냥 shared_ptr 궁금한 점을 테스트 해봄.

궁금증은 이러하다.

  • shared_ptr로 new를 할 수 있다. reference count 로 포인터 연결 갯수를 알 수 있다.

  • 기본 포인터로 new를 할 수 있다. 몇개의 포인터가 연결이 되었나 알 수 없다.

    • 그렇다면 기본 포인터로 new를 한 녀석을 shared_ptr이 받을 수는 없을까?

 

였다.

 

1. 무식하게 그냥 넣어본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <memory>
 
 
int main()
{
    // 일반 포인터로 Heap에 Instance 생성
    int* p = new int;
    
    // shared_ptr int타입으로 생성
    std::shared_ptr<int> shp;
    //shp = p;        // 불가
    
 
    //shared_ptr int타입으로 Heap에 Instance 생성
    std::shared_ptr<int> shp1(new int(3));
    shp = shp1;        // 가능
    shp1.reset();
 
    return 0;
}

→  12번째 줄처럼 불가.

당연히 shared_ptr은 class니까 하나 겠지만 서도.. -_-a

 

2. shared_ptr 내부를 뒤져봄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 
template<class _Ty>
    class _Ptr_base
    {    // base class for shared_ptr and weak_ptr
 
/// 중간생략
 
private:
    _Ty *_Ptr;
    _Ref_count_base *_Rep;
    template<class _Ty0>
        friend class _Ptr_base;
    };
 

→ 저기저 _Ptr이 private 이므로 접근이 불가 했음 ;;;

 

결론은 불가

 

 


 

 

반응형
반응형

BLOG main image



std::string directoryPath;


std::wstring wPullPathFilename;


wPullPathFilename.assign( directoryPath.begin(),directoryPath.end() );




예상하듯 string 에서 wstring 으로 변환은 괜찮지만


wstring 에서 string 으로 변환시 문자를 표현하는 특성상 제대로 표현되지 않을 수 있다

반응형
반응형

BLOG main image




특별히 추상클래스를 부모로 둘 필요가 없다 생각되고 각 다른타입으로 생성될 템플릿 클래스들이 자식으로 있을때

공통적으로 업캐스팅을 하고자 할때 다음처럼 사용


comment : 자식은 각 고유의 타입마다 다른 형태로 컴파일 타임에 인스턴스화 되어야 할때 사용

그외에도 목적에 맞게 사용하면 그만이지 뭐..






class objectBase_sjh{

public :

virtual ~objectBase_sjh(){};

virtual HRESULT renderGeoMatricalNormal(){ return S_OK; };                            //템플릿이 아닌 클래스에서의 가상함수

};





template<typename T>                                                                                            //템플릿이 아닌 클래스 objectBase_sjh 를 상속받지만

class objectCommon_sjh : public objectBase_sjh{                                                    //objectCommon_sjh  클래스는 템플릿 클래스

protected:

typedef std::vector<TEXTUREINFO_SJH> vecTextureInfo_sjh;                            

objectCommon_sjh(){}


public :

  //부모에서 정의한 virtual 을 따로 정의해줄 필요는 없다, 필요하다면 재정의


};




template<typename T>

class xFileObject_sjh : public objectCommon_sjh<T>{

public :

T _data;

public :

xFileObject_sjh(){

_data=60;

}

~xFileObject_sjh(){}

explicit xFileObject_sjh( LPDIRECT3DDEVICE9 pD3dDevice,  

std::wstring xFileName=_T(""), 

std::wstring textureName=_T(""), 

const std::wstring& directoryPath=_T("images/")   )

}


public :


virtual HRESULT renderGeoMatricalNormal(){ std::cout<<"xFileObject_sjh\t"<<_data<<std::endl;  return S_OK; }           //xFileObject_sjh 60      가 출력된다


};


//////////////////////////////////////////////////////////////////////////


template<typename T>                                                                                     //objectCommon_sjh 과는 다른 용도로 사용될 동일한 objectBase_sjh 를 상속받는

class objectOther_sjh : public objectBase_sjh{                                                  //템플릿클래스

protected:

typedef std::vector<TEXTUREINFO_SJH> vecTextureInfo_sjh;

objectOther_sjh(){}


public :




};



template<typename T>

class renderObject_sjh : public objectOther_sjh<T>{

public :

T _data;

public :

renderObject_sjh(){

_data=30.5;

}

~renderObject_sjh(){}

explicit renderObject_sjh( LPDIRECT3DDEVICE9 pD3dDevice,  

std::wstring xFileName=_T(""), 

std::wstring textureName=_T(""), 

const std::wstring& directoryPath=_T("images/")   )

}


public :



virtual HRESULT renderGeoMatricalNormal(){ std::cout<<"renderObject_sjh\t"<<_data<<std::endl;  return S_OK; }            //renderObject_sjh 30.5      가 출력된다


};



int main()

{

objectBase_sjh* instance1=new xFileObject_sjh<int>;


instance1->renderGeoMatricalNormal();



objectBase_sjh* instance2=new renderObject_sjh<float>;

instance2->renderGeoMatricalNormal();




getchar();


delete instance2;

delete instance1;


return 0;


}








반응형
반응형

http://www.hanb.co.kr/network/view.html?bi_id=1627


제공 : 한빛 네트워크
저자 : 최흥배


이전 회에는 STL 알고리즘들 중 비슷한 성격의 알고리즘을 모아서 '변경 불가 시퀀스 알고리즘', '변경 가능 시퀀스 알고리즘', '정렬 관련 알고리즘', '범용 수치 알고리즘'으로 크게 네 개로 나누고 이중 '변경 불가 시퀀스 알고리즘' 과 '변경 가능 시퀀스 알고리즘'에 있는 주요 알고리즘의 특징과 사용 방법에 대해서 설명하였습니다. 

이번 회는 이전 회에 설명하지 못한 '정렬 관련 알고리즘' 과 '범용 수치 알고리즘'의 주요 알고리즘의 특징과 사용 방법에 대해서 설명하겠습니다. 

10.1. 정렬 관련 알고리즘 

10.1.1 sort 

sort는 컨테이너에 있는 데이터들을 내림차순 또는 오름차순으로 정렬 할 때 가장 자주 사용하는 알고리즘입니다. 컨테이너에 저장하는 데이터의 자료 형이 기본형이라면 STL에 있는 greate나 less 비교 조건자를 사용합니다(STL의 string의 정렬에도 사용할 수 있습니다. 다만 이 때는 알파벳 순서로 정렬합니다). 기본형이 아닌 경우에는 직접 비교 조건자를 만들어서 사용해야 합니다. 

sort의 원형
template<class RandomAccessIterator>
   void sort( RandomAccessIterator _First, RandomAccessIterator _Last );
첫 번째와 두 번째 파라미터는 정렬하려는 구간의 시작과 마지막을 가리키는 반복자입니다. 비교 조건자를 필요로 하지 않고 기본형을 저장한 컨테이너를 오름차순으로 정렬합니다.
template<class RandomAccessIterator, class Pr>
   void sort( RandomAccessIterator _First, RandomAccessIterator _Last, BinaryPredicate _Comp );
첫 번째와 두 번째 파라미터는 정렬하려는 구간의 시작과 마지막을 가리키는 반복자입니다. 세 번째 파라미터는 정렬 방법을 기술한 비교 조건자입니다. 

sort 알고리즘의 원형을 보면 알 수 있듯이 램덤 접근 반복자를 지원하는 컨테이너만 sort 알고리즘을 사용할 수 있습니다. 

sort 사용 방법
vector<int> vec1;
…..
// 오름차순 정렬
sort( vec1.begin(), vec1.end() );

// 내림차순 정렬
Sort( vec1.begin(), vec1.end(), greater<int>() );
< 리스트 1. less와 greater 비교 조건자를 사용한 sort >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

int main()
{
  vector<int> vec1(10);
  vector<int> vec2(10);
  vector<int> vec3(10);
  vector <int>::iterator Iter1;

  generate( vec1.begin(), vec1.end(), rand );
  generate( vec2.begin(), vec2.end(), rand );
  generate( vec3.begin(), vec3.end(), rand );

  // 오름차순 정렬
  cout << "vec1 정렬 하기 전" << endl;
  for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  sort( vec1.begin(), vec1.end() );
  
  cout << "vec1 오름차순 정렬" << endl;
    for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;
  cout << endl;

  // 내림차순 정렬
  cout << "vec2 정렬 하기 전" << endl;
  for( Iter1 = vec2.begin(); Iter1 != vec2.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;
  
  sort( vec2.begin(), vec2.end(), greater<int>() );

  cout << "vec2 내림차순 정렬" << endl;
    for( Iter1 = vec2.begin(); Iter1 != vec2.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;
  cout << endl;

  // 일부만 내림차순 정렬
  cout << "vec3 정렬 하기 전" << endl;
  for( Iter1 = vec3.begin(); Iter1 != vec3.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  sort( vec3.begin() + 5, vec3.end(), greater<int>() );

  cout << "vec3 일부만 내림차순 정렬" << endl;
    for( Iter1 = vec3.begin(); Iter1 != vec3.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  return 0;
}
< 결과 > 

그림1 

<리스트 1>은 기본 형 데이터를 정렬하는 예제로 이번에는 유저 정의 형 데이터를 정렬하는 예제를 보여 드리겠습니다. 

< 리스트 2. USER 구조체의 Money를 기준으로 정렬 >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

struct USER
{
  int UID;
  int Level;
  int Money;
};

struct USER_MONEY_COMP
{
  bool operator()(onst USER& user1, const USER& user2)
  {
    return user1.Money > user2.Money;
  }
};

int main()
{
  USER User1; User1.UID = 1;  User1.Money = 2000;
  USER User2; User2.UID = 2;  User2.Money = 2050;
  USER User3; User3.UID = 3;  User3.Money = 2200;
  USER User4; User4.UID = 4;  User4.Money = 1000;
  USER User5; User5.UID = 5;  User5.Money = 2030;

  vector<USER> Users;
  Users.push_back( User1 );  Users.push_back( User2 );
  Users.push_back( User3 );  Users.push_back( User4 );
  Users.push_back( User5 );

  vector <USER>::iterator Iter1;

  cout << "돈을 기준으로 정렬 하기 전" << endl;
  for( Iter1 = Users.begin(); Iter1 != Users.end(); ++Iter1 ) {
    cout << Iter1->UID << " : " << Iter1->Money << ", ";
  }
  cout << endl << endl;

  sort( Users.begin(), Users.end(), USER_MONEY_COMP() );

  cout << "돈을 기준으로 내림차순으로 정렬" << endl;
  for( Iter1 = Users.begin(); Iter1 != Users.end(); ++Iter1 ) {
    cout << Iter1->UID << " : " << Iter1->Money << ", ";
  }
  cout << endl << endl;

  return 0;
}
< 결과 > 

그림2 

10.1.2. binary_search 

이미 정렬 되어 있는 것 중에서 특정 데이터가 지정한 구간에 있는지 조사하는 알고리즘입니다. 이것도 sort와 같이 비교 조건자가 필요 없는 버전과 필요한 버전 두 개가 있습니다(단 sort와 다르게 랜덤 접근 반복자가 없는 컨테이너도 사용할 수 있습니다). 

binary_search 원형
template<class ForwardIterator, class Type>
   bool binary_search( ForwardIterator _First, ForwardIterator _Last, const Type& _Val );

template<class ForwardIterator, class Type, class BinaryPredicate>
   bool binary_search( ForwardIterator _First, ForwardIterator _Last, const Type& _Val, 
                        BinaryPredicate _Comp );
binary_search 사용 방법
vector<int> vec1;
…..
sort( vec1.beign(), vec1.end() );
…...
bool bFind = binary_search( vec1.begin(), vec1.end(), 10 );
binary_search는 정렬한 이후에 사용해야 한다고 앞서 이야기 했습니다. 만약 정렬하지 않고 사용하면 어떻게 될까요? 

< 리스트 3. 정렬하지 않고 binary_search 사용 >
int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(15);
  vec1.push_back(7);  vec1.push_back(100); vec1.push_back(40);
  vec1.push_back(11);  vec1.push_back(60);  vec1.push_back(140);

  bool bFind = binary_search( vec1.begin(), vec1.begin() + 5, 15 );
  if( false == bFind ) {
    cout << "15를 찾지 못했습니다." << endl;
  } else {
    cout << "15를 찾았습니다." << endl;
  }

  return 0;
}
디버그 모드로 빌드 후 실행 하면 아래와 같은 에러 창이 나옵니다. 

그림3 

에러 내용은 시퀀스가 정렬되지 않았다고 나옵니다. 릴리즈 모드 빌드 후 실행하면 위와 같은 에러는 나오지 않지만 결과는 false가 나옵니다. 

binary_search를 사용할 때는 꼭 먼저 정렬해야 한다는 것을 잊지 말기를 바랍니다. 

< 리스트 4. 정렬 후 binary_search 사용 >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(15);
  vec1.push_back(7);  vec1.push_back(100); vec1.push_back(40);
  vec1.push_back(11);  vec1.push_back(60);  vec1.push_back(140);

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

  bool bFind = binary_search( vec1.begin(), vec1.begin() + 5, 15 );
  if( false == bFind ) {
    cout << "15를 찾지 못했습니다." << endl;
  } else {
    cout << "15를 찾았습니다." << endl;
  }

  return 0;
}
< 결과 > 

그림4 

비교 조건자를 사용하는 경우는 위의 <리스트 2> 코드를 예를들면 <리스트 2>에서 사용했던 USER_MONEY_COMP 조건자를 사용합니다. 

< 리스트 4. 리스트 2의 Users를 binary_search에 사용 >
…….
sort( Users.begin(), Users.end(), USER_MONY_COMP() );
bool bFind = binary_search( Users.begin(), Users.begin() + 3, User5, USER_MONY_COMP() );
…….
10.1.3 merge 

두 개의 정렬된 구간을 합칠 때 사용하는 것으로 두 구간과 겹치지 않은 곳에 합친 결과를 넣어야 합니다. 주의해야 할 점은 합치기 전에 이미 정렬이 되어 있어야 하며 합친 결과를 넣는 것은 합치는 것들과 겹치면 안되며, 또한 합친 결과를 넣을 수 있는 공간을 확보하고 있어야 합니다. 

merge의 원형
template<class InputIterator1, class InputIterator2, class OutputIterator>
   OutputIterator merge( InputIterator1 _First1, InputIterator1 _Last1,
      InputIterator2 _First2, InputIterator2 _Last2, OutputIterator _Result
   );

template<class InputIterator1, class InputIterator2, class OutputIterator, class BinaryPredicate>
   OutputIterator merge( InputIterator1 _First1, InputIterator1 _Last1,
    InputIterator2 _First2, InputIterator2 _Last2, OutputIterator _Result, BinaryPredicate _Comp );
merge 사용 방법
vector<int> vec1, vec2, vec3;
…..
merge( vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), vec3.begin() );
아래의 <리스트 5>는 vec1과 vec2를 합쳐서 vec3에 넣는 것으로 vec1과 vec2는 이미 정렬되어 있고 vec3는 이 vec1과 vec2의 크기만큼의 공간을 미리 확보해 놓고 있습니다. 

< 리스트 5. 두 개의 vector의 merge >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

int main()
{
  vector <int>::iterator Iter1;
  vector<int> vec1,vec2,vec3(12);
  
  for( int i = 0; i < 6; ++i )
    vec1.push_back( i );

  for( int i = 4; i < 10; ++i )
    vec2.push_back( i );
  
  cout << "vec1에 있는 값" << endl;
  for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  cout << "vec2에 있는 값" << endl;
  for( Iter1 = vec2.begin(); Iter1 != vec2.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;


  merge( vec1.begin(), vec1.end(), 
      vec2.begin(), vec2.end(), 
      vec3.begin() );

  cout << "vec1과 vec2를 merge한 vec3에 있는 값" << endl;
  for( Iter1 = vec3.begin(); Iter1 != vec3.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  return 0;
}
< 결과 > 

그림5 

정렬 관련 알고리즘은 위에 소개한 세 개 이외에도 더 있지만 지면 관계상 보통 자주 사용하는 sort, binary_search, merge 세 개를 소개하는 것으로 마칩니다. 제가 소개하지 않은 정렬 관련 알고리즘을 더 공부하고 싶은 분들은 MSDN이나 C++ 책을 통해서 공부하시기를 바랍니다. 

10.2. 범용 수치 알고리즘 

10.2.1 accumulate 

지정한 구간에 속한 값들을 모든 더한 값을 계산합니다. 기본적으로 더하기 연산만 하지만 조건자를 사용하면 더하기 이외의 연산도 할 수 있습니다. accumulate를 사용하기 위해서는 앞서 소개한 알고리즘과 다르게 <numeric> 헤더 파일을 포함해야 합니다. 

accumulate의 원형
template<class InputIterator, class Type>
   Type accumulate( InputIterator _First, InputIterator _Last, Type _Val );
첫 번째와 두 번째 파라미터는 구간이며, 세 번째 파라미터는 구간에 있는 값에 더할 값입니다.
template<class InputIterator, class Type, class BinaryOperation>
 Type accumulate( InputIterator _First, InputIterator _Last, Type _Val, BinaryOperation _Binary_op );
네 번째 파라미터는 조건자로 조건자를 사용하여 기본 자료 형 이외의 데이터를 더할 수 있고, 더하기 연산이 아닌 다른 연산을 할 수도 있습니다. 

accumulate 사용 방법
vector<int> vec1;
…..
// vec1에 있는 값들만 더 한다.
int Result = accmulate( vec1.begin(), vec1.end(), 0, );
아래의 <리스트 6>은 int를 저장하는 vector를 대상으로 accmurate를 사용하는 가장 일반적인 예입니다. 

< 리스트 6. vector에 있는 값들을 계산 >
#include <vector>
#include <iostream>
#include <numeric>

using namespace std;

int main()
{
  vector <int>::iterator Iter1;
  vector<int> vec1;

  for( int i = 1; i < 5; ++i )
    vec1.push_back( i );

  // vec1에 있는 값
  for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;


  // vec1에 있는 값들을 더한다.
  int Result1 = accumulate( vec1.begin(), vec1.end(), 0 );
  // vec1에 있는 값들을 더한 후 10을 더 한다.
  int Result2 = accumulate( vec1.begin(), vec1.end(), 10 );

  cout << Result1 << ", " << Result2 << endl;
}
< 결과 > 

그림6 

이번에는 조건자를 사용하여 유저 정의형을 저장한 vector를 accmulate에서 사용해 보겠습니다. 이번에도 더하기 연산만을 했지만 조건자를 사용하면 곱하기 연산 등도 할 수 있습니다. 

< 리스트 7. 조건자를 사용한 accumulate >
#include <vector>
#include <iostream>
#include <numeric>

using namespace std;

struct USER
{
  int UID;
  int Level;
  int Money;
};

struct USER_MONY_ADD
{
  USER operator()(const USER& user1, const USER& user2)
  {
    USER User;
    User.Money = user1.Money + user2.Money;
    return User;
  }
};

int main()
{
  USER User1; User1.UID = 1;  User1.Money = 2000;
  USER User2; User2.UID = 2;  User2.Money = 2050;
  USER User3; User3.UID = 3;  User3.Money = 2200;
  USER User4; User4.UID = 4;  User4.Money = 1000;
  USER User5; User5.UID = 5;  User5.Money = 2030;

  vector<USER> Users;
  Users.push_back( User1 );  Users.push_back( User2 );
  Users.push_back( User3 );  Users.push_back( User4 );
  Users.push_back( User5 );

  vector <USER>::iterator Iter1;

  for( Iter1 = Users.begin(); Iter1 != Users.end(); ++Iter1 ) {
    cout << Iter1->UID << " : " << Iter1->Money << ", ";
  }
  cout << endl << endl;

  // Users에 있는 Money 값만 더하기 위해 Money가 0인 InitUser를 세 번째 파라미터에
  // 조건자를 네 번째 파라미터로 넘겼습니다.
  USER InitUser; InitUser.Money = 0;
  USER Result = accumulate( Users.begin(), Users.end(), InitUser, USER_MONY_ADD() );
  cout << Result.Money << endl;
}
< 결과 > 

그림7 

10.2.2 inner_product 

두 입력 시퀀스의 내적을 계산하는 알고리즘으로 기본적으로는 +와 *을 사용합니다. 두 입력 시퀀스의 값은 위치의 값을 서로 곱한 값을 모두 더 한 것이 최종 계산 값이 됩니다. 주의 해야 할 것은 두 입력 시퀀스의 구간 중 두 번째 시퀀스는 첫 번째 시퀀스 구간 보다 크거나 같아야 합니다. 즉 첫 번째 시퀀스 구간의 데이터는 5개가 있는데 두 번째 시퀀스에 있는 데이터가 5개 보다 작으면 안됩니다. 

inner_product의 원형
template<class InputIterator1, class InputIterator2, class Type>
   Type inner_product( InputIterator1 _First1, InputIterator1 _Last1, InputIterator2 _First2, 
      Type _Val );
조건자를 사용하는 버전으로 조건자를 사용하면 유저 정의형을 사용할 수 있는 내적 연산 방법을 바꿀 수 있습니다.
template<class InputIterator1, class InputIterator2, class Type,
   class BinaryOperation1, class BinaryOperation2>
   Type inner_product( InputIterator1 _First1, InputIterator1 _Last1, InputIterator2 _First2, 
      Type _Val, BinaryOperation1 _Binary_op1, BinaryOperation2 _Binary_op2 );
아래의 <리스트 8>은 조건자를 사용하지 않는 inner_product를 사용하는 것으로 vec1과 vec2의 내적을 계산합니다. 

< 리스트 8. inner_product를 사용하여 내적 계산 >
#include <vector>
#include <iostream>
#include <numeric>

using namespace std;

int main()
{
  vector<int> vec1;
  for( int i = 1; i < 4; ++i )
    vec1.push_back(i);

  vector<int> vec2;
  for( int i = 1; i < 4; ++i )
    vec2.push_back(i);

  int Result = inner_product( vec1.begin(), vec1.end(), vec2.begin(), 0 );
  cout << Result << endl;

  return 0;
}
< 결과 > 

그림8 

<리스트 8>의 vec1과 vec2에는 각각 1, 2, 3 의 값이 들어가 있습니다. 이것을 네 번째 파라미터의 추가 값을 0을 넘긴 inner_droduct로 계산하면 

14 = 0 + (1 * 1) + (2 * 2) + (3 * 3); 

가 됩니다. 

<리스트 8>의 코드를 보기 전에는 어떻게 계산 되는지 잘 이해가 되지 않는 분들은 <리스트 8> 코드를 보면 inner_product가 어떻게 계산 되는지 쉽게 이해할 수 있을 것입니다. 

inner_product도 다른 알고리즘처럼 조건자를 사용할 수 있습니다. 제가 앞서 다른 알고리즘에서 조건자를 사용한 예를 보여 드렸으니 inner_product에서 조건자를 사용하는 방법은 숙제로 남겨 놓겠습니다.^^ 

범용 수치 알고리즘에는 위에 설명한 accmulate와 inner_product 이외에도 더 있지만 다른 것들은 보통 사용 빈도가 높지 않고 그것들을 다 소개하기에는 많은 지면이 필요로 하므로 이것으로 끝내겠습니다.^^; 

범용 수치 알고리즘을 끝으로 STL의 알고리즘을 설명하는 것을 마치겠습니다. 이전 회와 이번에 걸쳐서 소개한 알고리즘은 STL에 있는 알고리즘 중 사용 빈도가 높은 알고리즘들로 이 것 이외에도 많은 알고리즘이 있으니 제가 설명한 알고리즘을 공부한 후에는 제가 설명하지 않은 알고리즘들도 공부하시기를 바랍니다. 

지금까지 제가 설명한 글들을 보시면 STL은 사용할 때 일관성이 높다라는 것을 알 수 있을 것입니다. 높은 일관성 덕분에 하나를 알면 그 다음은 더 쉽게 알 수 있습니다. 

C++의 STL은 결코 사용하기 어려운 것이 아닙니다. C++을 알고 있다면 아주 쉽게 공부할 수 있으며 STL을 사용함으로 더 쉽고 견고하게 프로그래밍할 수 있습니다. 그러나 STL의 컨테이너나 알고리즘에 대해서 잘 모르면서 다른 사람들이 사용한 코드를 보고 그냥 사용하면 STL이 독이 될 수도 있음을 조심해야 합니다. 

저는 몇 달 전부터 C++0x을 공부하고 있습니다. C++0x는 현재 개발중인 새로운 C++ 표준입니다. C++0x에는 지금의 C++을 더 강력하고 쉽게 사용할 수 있게 해 주는 다양한 것들이 있습니다. 이 중 lambda라는 것이 있는데 이것을 사용하면 알고리즘에서 조건자를 사용할 때 지금보다 훨씬 더 편하게 기술할 수 있습니다. STL을 공부한 이 후에는 C++0x을 공부하시기를 바랍니다.

반응형
반응형

http://www.hanb.co.kr/network/view.html?bi_id=1626



제공 : 한빛 네트워크
저자 : 최흥배

제가 한빛 네트워크에 C++ STL 글을 연재한지 벌써 1년이 넘었습니다. template부터 시작하여 이전 회까지는 STL의 컨테이너들에 대해 설명했습니다. 이제 알고리즘에 대한 것만 남았습니다. 제가 쓴 글들을 보고 C++ STL을 공부하는데 얼마나 도움이 되는지 알 수 없지만 조금이나마 도움이 되었기를 바랍니다.
앞으로 2회에 걸쳐서 STL의 알고리즘 중 많이 사용하고 있는 것을 중심으로 설명하겠습니다.
STL의 컨테이너는 그 자체만으로도 대단히 유용한 것이지만 알고리즘과 결합되면 유용성은 더욱 더 커집니다.
STL의 알고리즘은 컨테이너처럼 제네릭(총칭적)합니다. 제네릭 하다고 하는 이유는 STL에 있는 다양한 알고리즘을 한 종류의 컨테이너에만 사용할 수 있는 것이 아닌 vector, list, deque, map 등 여러 가지 다양한 컨테이너에 사용할 수 있기 때문입니다(참고로 STL 컨테이너만이 아닌 일반 배열 구조도 알고리즘에 사용할 수 있습니다). 

9.1 STL 알고리즘 분류 

STL 알고리즘은 크게 네 가지로 분류 할 수 있습니다.

  • 변경 불가 시퀀스 알고리즘
  • 변경 가능 시퀀스 알고리즘
  • 정렬 관련 알고리즘
  • 범용 수치 알고리즘
변경 불가 시퀀스 알고리즘에는 find와 for_each 등이 있으며 대상 컨테이너의 내용을 변경하지 못합니다. 변경 가능 시퀀스 알고리즘으로는 copy, generate 등이 있으며 대상 컨테이너 내용을 변경합니다.
정렬 관련 알고리즘은 정렬이나 머지를 하는 알고리즘으로 sort와 merge 등이 있습니다.
범용 수치 알고리즘은 값을 계산하는 알고리즘으로 accumulate 등이 있습니다. 

STL의 모든 알고리즘을 설명하기에는 많은 지면이 필요하므로 위에 소개한 알고리즘 카테고리 별로 자주 사용하는 알고리즘 중심으로 어떤 기능이 있으며 어떻게 사용하는지 설명하겠습니다. 

9.2 조건자 

알고리즘 중에는 함수를 파라미터로 받아들이는 것이 많습니다. 알고리즘에서 함수를 파라미터로 받아들이지 않거나 기본 함수 인자를 사용하며 알고리즘을 사용하는 컨테이너의 데이터는 기본 자료 형만을 사용할 수 있습니다. 유저 정의형 자료 형(struct, class)을 담는 컨테이너를 알고리즘에서 사용하기 위해서는 관련 함수를 만들어서 파라미터로 넘겨줘야 합니다.
알고리즘에 파라미터로 넘기는 함수는 보통 함수보다는 함수객체를 사용하는 편입니다. 

9.3 변경 불가 시퀀스 알고리즘 

9.3.1. find 

컨테이너에 있는 데이터 중 원하는 것을 찾기 위해서는 find 알고리즘을 사용하면 됩니다.
참고로 알고리즘을 사용하려면 algorithm 헤더 파일을 추가해야 합니다.
#include < algorithm >
find의 원형
template<class InputIterator, class Type>
InputIterator find( InputIterator _First, InputIterator _Last, const Type& _Val );
첫 번째 파라미터에 찾기를 시작할 반복자 위치, 두 번째 파라미터에 마지막 위치(반복자의 end()와 같이 이 위치의 바로 앞까지 검색함), 세 번째 파라미터에는 찾을 값을 넘깁니다.
find가 성공하면 데이터가 있는 위치를 가리키는 반복자를 반환하고, 실패하면 반복자의 end()를 반환합니다. 

find 사용 방법
vector< int > ItemCodes;
……..
// ItemCodes 컨테이너의 시작과 끝 사이에서 15를 찾는다.
find( ItemCodes.begin(), ItemCodes.end(), 15 );
find를 사용하여 캐릭터의 아이템을 검색하는 예제 코드 입니다. 

< 리스트 1. 캐릭터 아이템 검색 >
#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;


int main()
{
  vector< int > CharItems;
  CharItems.push_back( 12 );
  CharItems.push_back( 100 );
  CharItems.push_back( 77 );

  vector< int >::iterator FindIter;

  // CharItems의 처음과 끝에서 12를 찾는다.
  FindIter = find( CharItems.begin(), CharItems.end(), 12 );
  if( FindIter != CharItems.end() )
  {
    cout << "CharItem 12를 찾았습니다." << endl;
  }
  else
  {
    cout << "CharItem 12는 없습니다" << endl;
  }

  // CharItems 두 번째와 끝에서12를 찾는다
  // ++CharItems.begin()로 반복자를 한칸 이동 시켰습니다.
        FindIter = find( ++CharItems.begin(), CharItems.end(), 12 );
  if( FindIter != CharItems.end() )
  {
    cout << "CharItem 12를 찾았습니다." << endl;
  }
  else
  {
    cout << "CharItem 12는 없습니다" << endl;
  }

  return 0;
}
< 결과 > 

1 

<리스트 1>에서는 vector 컨테이너를 대상으로 했지만 vector 이외의 list, deque도 같은 방식으로 사용합니다.
다만 map, set, hash_map의 연관 컨테이너는 해당 컨테이너의 멤버로 find()를 가지고 있으므로 알고리즘에 있는 find가 아닌 해당 멤버 find를 사용합니다. 

9.3.2 find_if 

find를 사용하면 컨테이너에 있는 데이터 중 찾기 원하는 것을 쉽게 찾을 수 있습니다. 그런데 find만으로는 많이 부족합니다. 이유는 find는 기본형만을 컨테이너에 저장하고 있을 때만 사용할 수 있습니다. 만약 유저 정의형을 저장하는 컨테이너는 find를 사용할 수 없습니다. 이럴 때는 9.2에서 짧게 설명한 조건자를 사용해야 합니다. 즉 조건자에 어떤 방식으로 찾을지 정의하고 알고리즘에서 이 조건자를 사용합니다.
find_if는 기본적으로 find와 같습니다. 다른 점은 조건자를 받아들인다는 것입니다. 

find_if 원형
template<class InputIterator, class Predicate>
InputIterator find_if( InputIterator _First, InputIterator _Last, Predicate _Pred );
find와 비교하면 첫 번째와 두 번째 파라미터는 동일합니다. 세 번째 파라미터가 다릅니다. find는 세 번째 파라미터에 찾기 원하는 값을 넘기지만 find_if는 조건자를 넘깁니다. 

find_if 사용법
// 컨테이너에 저장할 유저 정의형 
struct User
{
    …….
    int Money;
    int MobKillCount;
    ……
};

// 조건자
struct FindBestUser()
{
   …………………….
};

vector< User > Users;
……..

find_if( Users.begin(), Users.end(), FindBestUser() );
조건자가 있다는 것 이외에는 find_if는 find와 같으므로 조건자 부분만 잘 보시면 됩니다. 

< 리스트 2. 특정 돈을 가지고 있는 유저 찾기 >
#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;

struct User
{
  int Money;
  int Level;
};

struct FindMoneyUser
{
  bool operator() ( User& user ) const { return user.Money == ComparMoney; }
  int ComparMoney;
};

int main()
{
  vector< User > Users;

  User user1; user1.Level = 10; user1.Money = 2000;
  User user2; user2.Level = 5;  user2.Money = -10;
  User user3; user3.Level = 20; user3.Money = 35000;

  Users.push_back( user1 );
  Users.push_back( user2 );
  Users.push_back( user3 );

  vector< User >::iterator FindUser;

  FindMoneyUser tFindMoneyUser; 
  tFindMoneyUser.ComparMoney = 2000;
  FindUser = find_if( Users.begin(), Users.end(), tFindMoneyUser );
  if( FindUser != Users.end() )
  {
    cout << "소지하고 있는 돈은: " << FindUser->Money << "입니다" << endl;
  }
  else
  {
    cout << " 유저가 없습니다. " << endl;
  }

  return 0;
}
< 결과 > 

2 

<리스트 2>에서는 조건자 함수를 함수 포인터가 아닌 함수 객체를 사용하였습니다. 함수 객체를 사용한 이유는 함수 객체는 inline화 되므로 함수 포인터와 비교하면 함수 포인터 참조와 함수 호출에 따른 작업이 필요 없으며, 함수 객체를 사용하면 객체에 특정 데이터를 저장할 수 있어서 조건자가 유연해집니다. 

<리스트 2>의 FindMoneyUser를 일반 함수로 만들었다면 비교로 사용할 ComparMoney의 값은 함수를 정의할 때 고정됩니다. 그러나 함수 객체로 만들면 객체를 생성할 때 ComparMoney의 값을 설정할 수 있어서 유연해집니다. 

<참고>
조건자를 사용하는 알고리즘 : STL의 알고리즘은 조건자를 사용하지 않는 것과 조건자를 사용하는 알고리즘 두 가지 버전을 가지고 있는 알고리즘이 있습니다. 이중 조건자를 사용하는 알고리즘은 보통 조건자를 사용하지 않는 알고리즘의 이름에서 뒤에 '_if'를 붙인 이름으로 되어 있습니다. 

9.3.3. for_each 

for_each는 순차적으로 컨테이너들에 담긴 데이터를 함수의 파라미터로 넘겨서 함수를 실행시키는 알고리즘입니다.
온라인 게임에서 예를 들면 플레이하고 있는 유저 객체를 vector 컨테이너인 Users에 저장하고 모든 유저들의 현재 플레이 시간을 갱신하는 기능을 구현한다면 플레이 시간을 계산하는 함수 객체 UpdatePlayTime을 만든 후 for_each에 Users의 시작과 끝 반복자와 UpdatePlayTime 함수 객체를 넘깁니다. 

for_each 원형
template<class InputIterator, class Function>
Function for_each( InputIterator _First, InputIterator _Last, Function _Func );
첫 번째 파라미터는 함수의 파라미터로 넘길 시작 위치, 두 번째 파라미터는 함수의 인자로 넘길마지막 위치, 세 번째는 컨테이너의 데이터를 파라미터로 받을 함수입니다. 

for_each 사용 방법
// 함수 객체
struct UpdatePlayTime
{
……….
};

vector< User > Users;

for_each( Users.begin(), Users.end(), UpdatePlayTime );
아래는 위에서 예를 들었던 것을 완전한 코드로 구현한 예제 코드입니다. 

< 리스트 3. for_each를 사용하여 유저들의 플레이 시간 갱신 >
#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;

struct User
{
  int UID;
  int PlayTime;
};

struct UpdatePlayTime
{
  void operator() ( User& user )
  {
    user.PlayTime += PlayTime;
  }

  int PlayTime;
};

int main()
{
  vector< User > Users;

  User user1; user1.UID = 1; user1.PlayTime = 40000;
  User user2; user2.UID = 2; user2.PlayTime = 0;
  User user3; user3.UID = 3; user3.PlayTime = 25000;

  Users.push_back( user1 );
  Users.push_back( user2 );
  Users.push_back( user3 );

  // 현재 플레이 시간
  vector< User >::iterator IterUser;
  for( IterUser = Users.begin(); IterUser != Users.end(); ++IterUser )
  {
    cout << "UID : " << cout << IterUser->UID << "의 총 플레이 시간: " << IterUser->PlayTime << endl;
  }
  cout << endl;

  UpdatePlayTime updatePlayTime;
  updatePlayTime.PlayTime = 200;

  // 두 번째 유저부터 갱신
  for_each( Users.begin() + 1, Users.end(), updatePlayTime );
  
  for( IterUser = Users.begin(); IterUser != Users.end(); ++IterUser )
  {
    cout << "UID : " << cout << IterUser->UID << "의 총 플레이 시간: " << IterUser->PlayTime << endl;
  }

  return 0;
}
< 결과 > 

3 

변경 불가 시킨스 알고리즘에는 위에 설명한 find, find_if, for_each 이외에 count, search, equal, adjacent_find, equal 등이 있습니다. 

9.4. 변경 가능 시퀀스 알고리즘 

9.3의 알고리즘들이 대상이 되는 컨테이너에 저장한 데이터를 변경하지 않는 것들이라면 이번에 설명한 알고리즘들은 제목대로 대상이 되는 컨테이너의 내용을 변경하는 알고리즘들입니다. 컨테이너에 복사, 삭제, 대체 등을 할 때는 이번에 소개할 알고리즘들을 사용합니다. 

9.4.1. generate 

컨테이너의 특정 구간을 특정 값으로 채우고 싶을 때가 있습니다. 이 값이 동일한 것이라면 컨테이너의 assign() 멤버를 사용하면 되지만 동일한 값이 아니라면 assign()을 사용할 수 있습니다.
이 때 사용하는 알고리즘이 generate입니다. generate 알고리즘에 값을 채울 컨테이너의 시작과 끝, 값을 생성할 함수를 파라미터로 넘깁니다. 

generate 원형
template<class ForwardIterator, class Generator>
void generate( ForwardIterator _First, ForwardIterator _Last, Generator _Gen );
첫 번째 파라미터는 값을 채울 컨테이너의 시작 위치의 반복자, 두 번째는 컨테이너의 마지막 위치의 반복자, 세 번째는 값을 생성할 함수 입니다. 

generate 사용 방법
// 값 생성 함수
struct SetUserInfo
{
  …….
};

vector< User > Users(5);

generate( Users.begin(), Users.end(), SetUserInfo() );
generate 알고리즘의 대상이 되는 컨테이너는 값을 채울 공간이 미리 만들어져 있어야 합니다. 즉 generate는 컨테이너에 데이터를 추가하는 것이 아니고 기존의 데이터를 다른 값으로 변경하는 것입니다. 

< 리스트 4. generate를 사용하여 유저의 기초 데이터 설정>
struct User
{
  int UID;
  int RaceType;
  int Sex;
  int Money;
};

struct SetUserInfo
{
  SetUserInfo() { UserCount = 0; }

  User operator() ()
  {
    User user;

    ++UserCount;
    
    user.UID = UserCount;
    user.Money = 2000;

    if( 0 == (UserCount%2) )
    {
      user.RaceType = 1;
      user.Sex = 1;
      user.Money += 1000;
    }
    else
    {
      user.RaceType = 0;
      user.Sex = 0;
    }

    return user;
  }

  int UserCount;
};

int main()
{
  vector< User > Users(5);

  generate( Users.begin(), Users.end(), SetUserInfo() );
  
  char szUserInfo[256] = {0,};

  vector< User >::iterator IterUser;
  for( IterUser = Users.begin(); IterUser != Users.end(); ++IterUser )
  {
    sprintf( szUserInfo, "UID %d, RaceType : %d, Sex : %d, Money : %d",
    IterUser->UID, IterUser->RaceType, IterUser->Sex, IterUser->Money );

    cout << szUserInfo << endl;
  }
  
  return 0;
}
< 결과 > 

4 

9.4.2. copy 

copy 알고리즘은 컨테이너에 저장한 것과 같은 자료 형을 저장하는 다른 컨테이너에 복사하고 싶을 때 사용합니다. 

컨테이너 A의 데이터를 컨테이너 B에 copy 하는 경우 컨테이너 B에 데이터를 추가하는 것이 아니고 덧쓰는 것이므로 A에서 10개를 복사하는 경우 B에는 10개만큼의 공간이 없다면 버그가 발생합니다. 또 A와 B 컨테이너는 같은 컨테이너 필요는 없습니다만 당연히 컨테이너에 저장하는 자료 형은 같아야 합니다. 

copy 원형
template<class InputIterator, class OutputIterator>
   OutputIterator copy(
      InputIterator _First, 
      InputIterator _Last, 
      OutputIterator _DestBeg
   );
copy 사용 방법
vector<int> vec1;
……..
vector<int> vec2;

copy( vec1.begin(), vec1.end(), vec2.begin() );
< 리스트 5. copy 알골리즘 사용 예>
#include <algorithm>
#include <vector>
#include <list>
#include <iostream>

using namespace std;

int main()
{
  vector<int> vec1(10);
  generate( vec1.begin(), vec1.end(), rand );

  cout << "vec1의 모든 데이터를 vec2에 copy" << endl;

  vector<int> vec2(10);
  copy( vec1.begin(), vec1.end(), vec2.begin() );
  for( vector<int>::iterator IterPos = vec2.begin();
    IterPos != vec2.end();
    ++IterPos )
  {
    cout << *IterPos << endl;
  }
  cout << endl;


  cout << "vec1의 모든 데이터를 list1에 copy" << endl;
  list<int> list1(10);
  copy( vec1.begin(), vec1.end(), list1.begin() );
  
  for( list<int>::iterator IterPos2 = list1.begin();
    IterPos2 != list1.end();
    ++IterPos2 )
  {
    cout << *IterPos2 << endl;
  }

  return 0;
}
< 결과 > 

5 

9.4.4. remove 

remove 알고리즘은 컨테이너에 있는 특정 값들을 삭제하고 싶은 때 사용합니다.
주의해야 될 점은 삭제 후 크기가 변하지 않는다는 것입니다. 삭제가 성공하면 삭제 대상이 아닌 데이터들을 앞으로 몰아 넣고 마지막 위치의(컨테이너의 end()가 아닌 삭제 후 빈 공간에 다른 데이터를 쓰기 시작한 위치) 반복자를 반환합니다. 리턴 값이 가리키는 부분부터 끝(end()) 사이의 데이터 순서는 정의되어 있지 않으면 진짜 삭제를 하기 위해서는 erase()를 사용해야 합니다. 

remove 원형
template<class ForwardIterator, class Type>
ForwardIterator remove( ForwardIterator _First, ForwardIterator _Last, const Type& _Val );
첫 번째 파라미터는 삭제 대상을 찾기 시작할 위치의 반복자, 두 번째 파라미터는 삭제 대상을 찾을 마지막 위치의 반복자, 세 번째 파라미터는 삭제를 할 값입니다. 

remove 사용 방법
vector<int> vec1;
…..
remove( vec1.begin(), vec1.end(), 20 );
< 리스트 6. remove 사용 예>
#include <algorithm>
#include <vector>
#include <list>
#include <iostream>

using namespace std;

int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(20);
  vec1.push_back(40); vec1.push_back(50);  vec1.push_back(30);  

  vector<int>::iterator iterPos;

  cout << "vec1에 있는 모든 데이터 출력" << endl;
  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
  cout << endl;

  cout << "vec1에서 20 remove" << endl;
  vector<int>::iterator RemovePos = remove( vec1.begin(), vec1.end(), 20 );

  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
  cout << endl;

  cout << "vec1에서 20 remove 이후 사용 하지않는 영역 완전 제거" << endl;
  while( RemovePos != vec1.end() )
  {
    RemovePos = vec1.erase( RemovePos );
  }

  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
}
< 결과 > 

6 

<리스트 6>에서는 remove 후 erase()로 완전하게 제거하는 예를 나타내고 있습니다.
vector<int>::iterator RemovePos = remove( vec1.begin(), vec1.end(), 20 );
로 삭제 후 남은 영역의 반복자 위치를 받은 후
while( RemovePos != vec1.end() )
{
  RemovePos = vec1.erase( RemovePos );
}
로 완전하게 제거하고 있습니다. 

9.4.5. replace 

컨테이너의 특정 값을 다른 값으로 바꾸고 싶을 때는 replace 알고리즘을 사용합니다. 

replace 원형
template<class ForwardIterator, class Type>
   void replace(
      ForwardIterator _First, 
      ForwardIterator _Last,
      const Type& _OldVal, 
      const Type& _NewVal
   );
replace 사용 방법
vector<int> vec1;
……
replace( vec1.begin(), vec1.end(), 20, 30 );
< 리스트 7. replace 사용 예>
#include <algorithm>
#include <vector>
#include <list>
#include <iostream>

using namespace std;

int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(20);
  vec1.push_back(40); vec1.push_back(50);  vec1.push_back(30);

  vector<int>::iterator iterPos;

  cout << "vec1에 있는 모든 데이터 출력" << endl;
  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
  cout << endl;

  cout << "vec1의 세 번째 요소부터 20을 200으로 변경" << endl;
  replace( vec1.begin() + 2, vec1.end(), 20, 200 );

  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }

  return 0;
}
< 결과 > 

7 

변경 가능 시퀀스 알고리즘에는 generate, copy, remove, replace 이외에도 fill, swap, reverse, transform 등이 있으니 제가 설명하지 않은 알고리즘들은 다음에 여유가 있을 때 공부하시기를 바랍니다. 

지금까지 설명한 알고리즘을 보면 어떤 규칙이 있다는 것을 알 수 있을 것입니다. 알고리즘에 넘기는 파라미터는 알고리즘을 적용할 컨테이너의 위치를 가리키는 반복자와 특정 값 or 조건자를 파라미터로 넘기고 있습니다. 그래서 각 알고리즘은 이름과 기능이 다를 뿐 사용 방법은 대체로 비슷합니다. 즉 사용 방법에 일괄성이 있습니다. 그래서 하나의 알고리즘만 사용할 줄 알게 되면 나머지 알고리즘은 쉽게 사용하는 방법을 배웁니다. 이런 것이 바로 STL의 장점이겠죠 

이번 회는 이것으로 설명을 마치고 남은 알고리즘은 다음 회에 이어서 계속 설명하겠습니다.

반응형
반응형

http://www.hanb.co.kr/network/view.html?bi_id=1624




제공 : 한빛 네트워크
저자 : 최흥배

앞서 설명했던 map과 비슷하면서도 다른 set을 이번에 이야기 하려고 합니다. map 이전에는 hash_map을 설명했는데 이번에 이야기할 set과 여러 부분에서 중복되는 부분이 있고, 저는 현업에서 set을 사용하는 경우가 거의 없어서 이번에는 내용이 길지 않을 것 같습니다. 

8.1 set 이란 

set은 원하는 key를 신속하게 찾고, 또 이 key가 정렬되기를 원할 때 사용합니다.(여기서 말하는 key라는 것은 저장할 자료를 말합니다). map과 비슷하지만 다른 점은 map은 key와 값이 한 쌍으로 저장하지만 set은 key만 저장합니다. set도 map과 같이 key를 중복으로 저장할 수 없습니다. 만약 key를 중복으로 사용하고 싶다면 multiset을 사용해야 합니다. 사용방법은 set과 거의 같습니다. set은 map과 같이 이진 탐색 트리 자료구조를 사용합니다. 

8.2 set을 사용할 때 

앞서 이야기 했듯이 set은 자료를 저장할 때 내부에서 자동으로 정렬하고, map과 다르게 key만 저장합니다. 

set은 아래 조건일 때 사용하면 좋습니다.

  1. 정렬해야 한다.
  2. key가 있는지 없는지 알아야 할 때
  3. 많은 자료를 저장하고, 검색 속도가 빨라야 할 때
다른 컨테이너를 설명 할 때 꽤 많은 이야기를 했는데 그동안 했던 이야기와 중복되는 것이 많고 특히 앞의 map과 비슷한 부분이 많아서 이번에는 바로 사용 방법으로 들어가겠습니다. 

8.3 set 사용 방법 

set 컨테이너를 쓰려면 먼저 헤더 파일을 포함해야 합니다.
#include <set>
보통 set을 사용하는 방법은 아래와 같습니다.
set< key 자료 type > 변수 이름

set< int > set1;
map과 사용 방법이 비슷하죠? 다만, set은 위와 같이 key만 저장합니다. 위에서는 key로 int 타입을 사용했습니다. 

set은 map과 같이 기본적으로 오름차순으로 정렬을 합니다. 만약 이것을 내림 차순으로 바꾸고 싶거나 key 자료형이 기본형이 아니란 유저 정의형이라면 함수 객체로 정렬 방법을 제공해야 합니다. 

set의 key가 기본형이고 내림 차순으로 정렬하고 싶다면 STL의 greater 알고리즘을 사용하면 됩니다.
set< key 자료 type, 비교 함수 > 변수 이름

set< int, greater<int> > set1;
만약 key가 기본형이 아니고 Player 이라는 클래스를 사용하고 Player의 멤버 중 HP를 비교하여 정렬하고 싶다면 아래와 같이 하면 됩니다.
class Player
{
public:
   Player() {}
   ~Player() {}

   int m_HP;
};

template< typename T >
struct HP_COMPARE : public binary_function< T, T, bool >
{
   bool operator() (T& player1, T& player2) const 
   { 
      return player1.m_HP > player2.m_HP;
   }
};

int main()
{
   set< Player, HP_COMPARE<Player> > set1;
   return 0;
}
8.3.1 set의 주요 멤버들 

멤버설명
begin첫 번째 원소의 랜덤 접근 반복자를 반환
clear저장하고 있는 모든 원소를 삭제
empty저장 하고 있는 요소가 없으면 true 반환
end마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase특정 위치의 원소나 지정 범위의 원소들을 삭제
findkey와 연관된 원소의 반복자 반환
insert원소 추가
lower_bound지정한 key의 요소를 가지고 있다면 해당 위치의 반복자를 반환
operator[]지정한 key 값으로 원소 추가 및 접근
rbegin역방향으로 첫 번째 원소의 반복자를 반환
rend역방향으로 마지막 원소 다음의 반복자를 반환
size원소의 개수를 반환
upper_bound지정한 key 요소를 가지고 있다면 해당 위치 다음 위치의 반복자 반환
[표 1]. map의 주요 멤버들 

8.3.2. 추가 

set 에서는 자료를 추가 할 때 insert를 사용합니다.
원형 : 
  pair <iterator, bool> insert( const value_type& _Val );
  iterator insert( iterator _Where, const value_type& _Val );
  template<class InputIterator> void insert( InputIterator _First, InputIterator _Last );
첫 번째가 자주 사용하는 방식입니다.
set< int > set1;

// key 1을 추가.
set1.insert( 1 );

// 추가했는지 조사 하고 싶을 때는
pair< set<int>::iterator, bool > Result;
Result = set1.insert( 1 );
if( Result.second )
{
   // 추가 성공
}
else
{
  // 추가 실패
}
두 번째 방식은 특정 위치에 추가할 수 있습니다.
// 첫 번째 위치에 key 1, value 35를 추가
set1.insert( set1.begin(), 10 );
세 번째 방식은 지정한 반복자 구간에 있는 것들을 추가합니다.
set< int > set2;
// set1의 모든 요소를 set2에 추가.
set2.insert( set1.begin(), set1.end() );
set은 이미 있는 key 값을 추가할 수 없습니다(복수의 key 값을 사용하기 위해서는 multiset을 사용해야 합니다). 

참고로 특정 위치를 지정하여 추가를 하여도 정렬되어 저장합니다. 

<리스트 1. 특정 위치에 추가했을 때의 정렬 여부>
#include <iostream>
#include <functional>
#include <set>

using namespace std;

int main()
{
   set< int > set1;
   set1.insert( 10 );
   set1.insert( 15 );
   set1.insert( 12 );
   set1.insert( 2 );
   set1.insert( 100 );

   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }

   set<int>::iterator IterPos = set1.begin();
   ++IterPos;
   set1.insert( IterPos, 90 );
   
   cout << endl;
   cout << "90을추가후set1의모든요소출력" << endl;
   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }
   
   return 0;
}
<결과> 

그림1 

8.3.3. 반복자 사용 

다른 컨테이너와 같이 정 방향 반복자 begin(), end()와 역 방향 반복자 rbegin(), rend()를 지원합니다. 사용 방법은 다음과 같습니다.
// 정 방향으로 set1의 모든 Key 출력
set< int >::iterator Iter_Pos;
for( Iter_Pos = set1.begin(); Iter_Pos != set1.end(); ++Iter_Pos)
{
   cout << *Iter_Pos << endl;
}

// 역 방향으로 set1의 모든 요소 Key 출력
set< int >::reverse_iterator Iter_rPos;
for( Iter_rPos = set1.rbegin(); Iter_rPos != set1.rend(); ++Iter_rPos)
{
   cout << *Iter_rPos << endl;
}
8.3.4. 검색 

set에서 검색은 key를 대상으로 합니다.
key와 같은 요소를 찾으면 그 요소의 반복자를 반환하고, 찾지 못한 경우에는 end()를 가리키는 반복자를 반환합니다.
원형 : 
  iterator find( const Key& _Key );
  const_iterator find( const Key& _Key ) const;
두 방식의 차이는 반환된 반복자가 const 여부입니다. 첫 번째 방식은 const가 아니므로 찾은 요소의 Key를 변경할 수 있습니다. 그러나 두 번째 방식은 Key를 변경할 수 없습니다.
// key가 10인 요소 찾기.
set< int >::Iterator FindIter = set1.find( 10 );

// 찾았다면 value를 1000으로 변경
if( FindIter != set1.end() )
{
   // Key를 찾았다!!!
}
set은 map과 다르게 Key만 저장하기에 Key의 변경이 가능하지만 find로 찾은 Key를 변경하면 정렬되지 않습니다. 

<리스트 2. find로 찾은 Key 변경>
int main()
{
   set< int > set1;
   set1.insert( 10 );
   set1.insert( 15 );
   set1.insert( 12 );

   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }

   set<int>::iterator FindIter = set1.find( 15 );
   if( FindIter != set1.end() )
   {
      *FindIter = 11;
   }

   cout << endl;
   cout << "15를 검색 후11로 변경한 후 set1의 모든 요소 출력" << endl;
   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }

   return 0;
}
<결과> 

그림2 

8.3.5. 삭제 

저장하고 있는 요소를 삭제할 때는 erase와 clear를 사용합니다.
erase는 특정 요소를 삭제할 때 사용하고, clear는 모든 요소를 삭제할 때 사용합니다. 

erase
원형 : 
  iterator erase( iterator _Where );
  iterator erase( iterator _First, iterator _Last );
  size_type erase( const key_type& _Key );
첫 번째 방식은 특정 위치에 있는 요소를 삭제합니다.
// 두 번째 위치의 요소 삭제.
set1.erase( ++set1.begin() );
두 번째 방식은 지정한 구역 에 있는 요소들을 삭제합니다.
// set1의 처음과 마지막에 있는 모든 요소 삭제
set1.erase( set1.begin(), set1.end() );
세 번째 방식은 지정한 Key와 같은 요소를 삭제합니다.
// key가 10인 요소 삭제.
set1.erase( 10 );
첫 번째와 두 번째 방식으로 삭제를 하면 삭제되는 요소의 다음을 가리키는 반복자를 반환합니다.
세 번째 방식은 삭제된 개수를 반환하는데 정말 삭제가 되었다면 1이 반환됩니다. 그러나 multiset에서는 1이 아닌 삭제된 개수를 반환합니다. 

clear 

set의 모든 요소를 삭제할 때는 clear를 사용합니다.
set1.clear();
이것으로 set에서 자주 사용하는 멤버들에 대한 설명은 끝났습니다. 
아래의 <리스트 3>은 set을 전반적으로 사용하는 예를 나타내고 있습니다. 

<리스트 3. set 사용 예>
#include <iostream>
#include <functional>
#include <set>

using namespace std;

class Player
{
public:
   Player() {}
   ~Player() {}

   int m_Level;
};

// 레벨이 높은 순으로 정렬
template< typename T > 
struct LEVEL_COMPARE : public binary_function< T, T, bool >
{
   bool operator() (const T& player1, const T& player2) const 
   { 
      return player1->m_Level > player2->m_Level;
   }
};

int main()
{
   set< Player*, LEVEL_COMPARE<Player*> > PlayerList;

   Player* pPlayer1 = new Player; pPlayer1->m_Level = 10;
   PlayerList.insert( pPlayer1 );
   Player* pPlayer2 = new Player; pPlayer2->m_Level = 45;
   PlayerList.insert( pPlayer2 );
   Player* pPlayer3 = new Player; pPlayer3->m_Level = 5;
   PlayerList.insert( pPlayer3 );
   Player* pPlayer4 = new Player; pPlayer4->m_Level = 15;
   PlayerList.insert( pPlayer4 );

   // 정 방향으로 출력( 레벨이 높은 순으로)
   for( set< Player*, LEVEL_COMPARE<Player*> >::iterator IterPos = PlayerList.begin();
      IterPos != PlayerList.end(); ++IterPos )
   {
      cout << (*IterPos)->m_Level << endl;
   }

   cout << endl;

   // 역 방향으로 출력( 레벨이 낮은 순으로)
   for( set< Player*, LEVEL_COMPARE<Player*> >::reverse_iterator IterPos = PlayerList.rbegin();
      IterPos != PlayerList.rend(); ++IterPos )
   {
      cout << (*IterPos)->m_Level << endl;
   }

   cout << endl;

   // pPlayer4를검색
   set< Player*, LEVEL_COMPARE<Player*> >::iterator FindPlayer = PlayerList.find( pPlayer4 );
   if( FindPlayer != PlayerList.end() )
   {
      cout << "pPlayer4를 찾았습니다" << endl;

      cout << "pPlayer4 삭제" << endl;
      PlayerList.erase( FindPlayer );
   }
   else
   {
      cout << "pPlayer4를 못찾았습니다" << endl;
   }

   cout << endl;
   cout << "Total Player Count : " << PlayerList.size() << endl;

   cout << endl;
   PlayerList.clear();
   if( PlayerList.empty() )
   {
      cout << "Player가 없습니다." << endl;
   }
   
   delete pPlayer1;
   delete pPlayer2;
   delete pPlayer3;
   delete pPlayer4;
   return 0;
}
<결과> 

그림3 

이전 회의 map과 거의 대부분 비슷하기 때문에 map을 아시는 분들은 아주 쉬웠을 것이라고 생각합니다. 

이전과 같이 set의 멤버 중 설명하지 않은 것들은 MSDN에 있는 set 설명을 참조하시기를 바랍니다. 

과제 

Sset은 Key를 중복으로 저장할 수 없습니다. 중복 Key를 저장하기 위해서는 multiset을 사용해야 합니다. multiset을 공부해 보세요 

참고 url : http://msdn.microsoft.com/ko-kr/library/w5txk7zc.aspx

반응형
반응형



제공 : 한빛 네트워크
저자 : 최흥배

http://www.hanb.co.kr/network/view.html?bi_id=1618


이번에는 이전 회에 설명한 hash_map과 같은 연관 컨테이너 중의 하나인 map에 대해서 설명합니다. 사용법이 hash_map과 대부분 비슷해서 앞으로 할 이야기가 별로 어렵지 않을 것입니다. 

이전 회에서 설명했던 것은 가급적 또 설명하지 않을 테니 앞의 글들을 보지 않은 분들은 꼭 봐 주세요. 그럼 map에 대한 이야기를 시작 하겠습니다. 

7.1 map의 자료구조 

map의 자료구조는 '트리(tree)'입니다(정확하게 말하면 트리 자료구조 중의 하나인 '레드-블랙 트리(Red-Black tree)'입니다). 

트리는 한글로 '나무'입니다. 나무는 뿌리에서 시작하여 여러 갈래의 가지가 있고, 가지의 끝에는 나무 잎이 있습니다. 트리 자료구조도 이와 같은 형태를 가지고 있어서 루트(root), 리프(leaf)라는 용어를 사용합니다. 

그림1 
[그림 1] 실제 나무(왼쪽)와 트리 자료구조의 모습(오른쪽) 

오른쪽의 트리 자료구조에서 제일 최상 위의 '5'는 루트 노드(root node)라고 하며, 노드'5'와 노드'7'의 관계에서 노드'5'는 부모 노드(parent node), 노드'7'은 자식 노드(child node)라고 합니다. 또한 노드 '12'와 노드 '30'의 관계에서는 부모 노드는 노드'12'입니다. 자식이 없는 노드는 리프 노드(leaf node)라고 합니다. 그림 1에서는 '9', '30', '35', '20'이 리프 노드입니다. 

7.2 트리 자료구조의 특징 

트리는 노드를 균형 있게 가지는 것이 성능에 유리하기 때문에 기본 트리에서 변형된 B-트리, B+ 트리, R-트리, 레드 블랙 트리, AVL 트리 등 다양한 종류의 트리 자료구조가 있습니다. 

균형을 이룬 트리는 자료를 정해진 방식에 따라서 분류하여 저장하기 때문에 시퀸스(일렬로)하게 자료를 저장하는 연결 리스트에 비해서 검색이 빠릅니다. 그렇지만 정해진 규칙에 따라서 자료를 삽입, 삭제 해야 되기 때문에 삽입과 삭제가 간단하지 않으며 구현이 복잡합니다. 

7.3 map을 언제 사용해야 될까? 

map은 많은 자료를 정렬하여 저장하고 있고 빠른 검색을 필요로 할 때 자주 사용합니다. 많은 자료를 빠르게 검색한다고 하는 부분은 앞 회에서 설명한 hash_map과 비슷합니다. 그러나 hash_map과 크게 다른 부분이 있습니다. map은 자료를 저장할 때 내부에서 자동으로 정렬을 하고, hash_map은 정렬하지 않는다라는 것입니다. 정렬이 필요하지 않는 곳에서 map을 사용하는 것은 불 필요한 낭비입니다. 

map은 아래 조건일 때 사용하면 좋습니다. 

1. 정렬해야 한다.
2. 많은 자료를 저장하고, 검색이 빨라야 한다
3. 빈번하게 삽입, 삭제하지 않는다. 

7.4 map 사용 방법 

가장 먼저 map의 헤더파일을 포함합니다.

#include 
보통 map을 사용하는 방법은 아래와 같습니다.
map< key 자료 type, value 자료 type > 변수 이름

map< int, int > map1;
value는 저장할 자료이고, key는 value를 가리키는 것입니다. 위에서는 key의 자료형 int, value 자료형 int인 map을 생성합니다. 

앞에서 map은 자료를 저장할 때 정렬을 한다고 말했습니다. 정렬의 대상은 key를 대상으로 하며오름차순으로 정렬합니다. 그래서 내림차순으로 정렬하고 싶거나 key의 자료형이 기본형이 아닌 유저 정의형(class나 struct로 정의한 것)인 경우는 정렬 방법을 제공해야 합니다. 

위에 생성한 map1은 오름차순으로 정렬하는데 이것을 내림차순으로 정렬하고 싶다면 아래와 같이 하면 됩니다.
map< key 자료 type, value 자료 type, 비교 함수 > 변수 이름

map< int, int, greater<int> > map1;
위에서 사용한 비교 함수 greater는 제가 따로 만든 것이 아니고 STL에 있는 템플릿입니다. 

greater와 같은 것을 STL 알고리즘 이라고 하는데 이것들은 다음 시간에 자세하게 설명할 예정이니 여기서는 이런 것이 있다는 것만 아시면 됩니다. 

다른 컨테이너와 같이 map도 동적 할당을 할 수 있습니다. 사용 방법은 앞서 소개한 컨테이너들과 비슷합니다. 

앞 회의 hash_map과 비교를 하면 사용 방법이 거의 같다라는 것을 알 수 있습니다. 이후 소개하는 map의 멤버함수도 일부분만 제외하고는 hash_map과 같습니다. 이전에도 이야기 했지만 서로 다른 컨테이너가 사용방법이 서로 비슷하여 하나만 제대로 배우 나머지 것들도 배우기 쉽다라는 것이 STL의 장점 중의 하나입니다. 

7.4.1 map의 주요 멤버들 

멤버설명
begin첫 번째 원소의 랜덤 접근 반복자를 반환
clear저장하고 있는 모든 원소를 삭제
empty저장 하고 있는 요소가 없으면 true 반환
End마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase특정 위치의 원소나 지정 범위의 원소들을 삭제
Findkey와 연관된 원소의 반복자 반환
insert원소 추가
lower_bound지정한 key의 요소를 가지고 있다면 해당 위치의 반복자를 반환
operator[]지정한 key 값으로 원소 추가 및 접근
rbegin역방향으로 첫 번째 원소의 반복자를 반환
rend역방향으로 마지막 원소 다음의 반복자를 반환
size원소의 개수를 반환
upper_bound지정한 key 요소를 가지고 있다면 해당 위치 다음 위치의 반복자 반환

[표 1] map의 주요 멤버들 

7.4.2. 추가 

map 에서는 자료를 추가 할 때 insert를 사용합니다.
원형 : 
pair <iterator, bool> insert( const value_type& _Val );
iterator insert( iterator _Where, const value_type& _Val );
template<class InputIterator> void insert( InputIterator _First, InputIterator _Last );
첫 번째 방식이 보통 가장 자주 사용하는 방식입니다.
map< int, int > map1;

// key는 1, value는 35를 추가.
map1.insert( map< int, int >::value_type(1, 35));

// 또는 STL의 pair를 사용하기도 합니다.
typedef pair < int, int > Itn_Pair;
map1.insert(  Int_Pair(2, 45) );
두 번째 방식으로는 특정 위치에 추가할 수 있습니다.
// 첫 번째 위치에 key 1, value 35를 추가
map1.insert( map1.begin(), map< int, int >::value_type(1, 35) );

// 또는
map1.insert( map1.begin(),  Int_Pair(2, 45) );
세 번째 방식으로는 지정한 반복자 구간에 있는 것들을 추가합니다.
map< int, int > map2;
// map1의 모든 요소를 map2에 추가.
map2.insert( map1.begin(), map1.end() );
map은 이미 있는 key 값을 추가할 수 없습니다(복수의 key 값을 사용하기 위해서는 multi_map을 사용해야 합니다). 가장 자주 사용하는 첫 번째 방식으로 추가하는 경우는 아래와 같은 방법으로 결과를 알 수 있습니다.
pair< map<int, int>::iterator, bool > Result;
Result = map1.insert( Int_Pair(1, 35));
만약 이미 key 값 1이 추가 되어 있었다면 insert 실패로 Result.second 는 false이며, 반대로 성공하였다면 true 입니다. 

operator[]를 사용하여 추가하기 

insert가 아닌 operator[]를 사용하여 추가할 수도 있습니다.
// key 10, value 80을 추가
map1[10] = 80;
7.4.3. 반복자 사용 

다른 컨테이너와 같이 정 방향 반복자 begin(), end()와 역 방향 반복자 rbegin(), rend()를 지원합니다. 

사용 방법은 다음과 같습니다.
// 정 방향으로 map1의 모든 요소의 value 출력
map< int, int >::iterator Iter_Pos;
for( Iter_Pos = map1.begin(); Iter_Pos != map1.end(); ++Iter_Pos)
{
   cout << Iter_Pos.second << endl;
}

// 역 방향으로 map1의 모든 요소의 value 출력
map< int, int >::reverse_iterator Iter_rPos;
for( Iter_rPos = map1.rbegin(); Iter_rPos != map1.rend(); ++Iter_rPos)
{
   cout << Iter_rPos.second << endl;
}
위에서 map을 정의할 때 비교함수를 사용할 수 있다고 했습니다. 만약 비교함수를 사용한 경우는 반복자를 정의할 때도 같은 비교함수를 사용해야 합니다.
map< int, int, greater<int> > map1;
map< int, int, greater<int> >::iterator Iter_Pos;
7.4.4. 검색 

map에서 검색은 key 값을 대상으로 합니다. key와 같은 요소를 찾으면 그 요소의 반복자를 반환하고, 찾지 못한 경우에는 end()를 가리키는 반복자를 반환합니다.
원형 : 
iterator find( const Key& _Key );
const_iterator find( const Key& _Key ) const;
두 방식의 차이는 반환된 반복자가 const냐 아니냐는 차이입니다. 첫 번째 방식은 const가 아니므로 찾은 요소의 value를 변경할 수 있습니다(참고로 절대 key는 변경 불가입니다). 그러나 두 번째 방식은 value를 변경할 수 없습니다.
// key가 10인 요소 찾기.
map< int, int >::Iterator FindIter = map1.find( 10 );

// 찾았다면 value를 1000으로 변경
if( FindIter != map1.end() )
{
   FindIter->second = 1000;
}
7.4.5. 삭제 

저장하고 있는 요소를 삭제할 때는 erase와 clear를 사용합니다. erase는 특정 요소를 삭제할 때 사용하고, clear는 모든 요소를 삭제할 때 사용합니다. 

erase
원형 : 
iterator erase( iterator _Where );
iterator erase( iterator _First, iterator _Last );
size_type erase( const key_type& _Key );
첫 번째 방식은 특정 위치에 있는 요소를 삭제합니다.
// 두 번째 위치의 요소 삭제.
map1.erase( ++map1.begin() );
두 번째 방식은 지정한 구역에 있는 요소들을 삭제합니다.
// map1의 처음과 마지막에 있는 모든 요소 삭제
map1.erase( map1.begin(), map1.end() );
세 번째 방식은 지정한 키와 같은 요소를 삭제합니다.
// key가 10인 요소 삭제.
map1.erase( 10 );
첫 번째와 두 번째 방식에서는 삭제하는 요소의 다음을 가리키는 반복자를 반환하고(C++ 표준에서는 반환하지 않습니다만 Microsoft의 Visual C++에서는 반환합니다), 세 번째 방식은 삭제된 개수를 반환합니다. map에서는 세 번째 방식으로 삭제를 하는 경우 정말 삭제가 되었다면 무조건 1이지만, multi_map에서는 삭제한 개수만큼의 숫자가 나옵니다. 

clear 

map의 모든 요소를 삭제할 때는 clear를 사용합니다.
map1.clear();
이것으로 map에서 자주 사용하는 멤버들에 대한 설명은 끝났습니다. [표 1]에 나와 있는 멤버들 중 사용 방법이 간단한 것은 따로 설명하지 않으니 [리스트 1]의 코드를 봐 주세요. 

[리스트 1] 정렬된 아이템 리스트 출력
#include <map>
#include <string>
#include <iostream>

using namespace std;

struct Item
{
  char Name[32];  // 이름
  char Kind;  // 종류
  int BuyMoney;  // 구입 가격
  int SkillCd;  // 스킬 코드
};

int main()
{
  map< char*, Item > Items;
  map< char*, Item >::iterator IterPos;
  typedef pair< char*, Item > ItemPair;

  Item Item1;
  strncpy( Item1.Name, "긴칼", 32 );
  Item1.Kind = 1;    Item1.BuyMoney = 200;  Item1.SkillCd = 0;

  Item Item2;
  strncpy( Item2.Name, "성스러운 방패", 32 );
  Item2.Kind = 2;    Item2.BuyMoney = 1000;  Item2.SkillCd = 4;

  Item Item3;
  strncpy( Item3.Name, "해머", 32 );
  Item3.Kind = 1;    Item3.BuyMoney = 500;  Item3.SkillCd = 0;

  // Items에 아이템 추가
  Items.insert( map< char*, Item >::value_type(Item2.Name, Item2) );
  Items.insert( ItemPair(Item1.Name, Item1) );

  // Items가 비어 있지않다면
  if( false == Items.empty() )
  {
    cout << "저장된 아이템 개수- " << Items.size() << endl;
  }

  for( IterPos = Items.begin(); IterPos != Items.end(); ++IterPos )
  {
    cout << "이름: " << IterPos->first << ", 가격: " << IterPos->second.BuyMoney << endl;
  }

  IterPos = Items.find("긴칼");
  if( IterPos == Items.end() )   {
    cout << "아이템'긴칼'이 없습니다." << endl;
  }
  cout << endl;

  cout << "올림차순으로 정렬되어있는 map(Key 자료형으로string 사용)" << endl;
  
  map< string, Item, less<string> > Items2;
  map< string, Item, less<string> >::iterator IterPos2;
  
  Items2.insert( map< string, Item >::value_type(Item2.Name, Item2) );
  Items2.insert( ItemPair(Item1.Name, Item1) );
  // operator[]를 사용하여 저장
  Items2[Item3.Name] = Item3;

   for( IterPos2 = Items2.begin(); IterPos2 != Items2.end(); ++IterPos2 )
  {
    cout << "이름: " << IterPos2->first << ", 가격: " << IterPos2->second.BuyMoney << endl;
  }
  cout << endl;

  cout << "해머의 가격은 얼마? ";
  IterPos2 = Items2.find("해머");
  if( IterPos2 != Items2.end() )   {
    cout << IterPos2->second.BuyMoney << endl;
  }
  else {
    cout << "해머는 없습니다" << endl;
  }
  cout << endl;

  // 아이템 "긴칼"을 삭제한다.
  IterPos2 = Items2.find("긴칼");
  if( IterPos2 != Items2.end() )   {
    Items2.erase( IterPos2 );
  }

  cout << "Items2에 있는 아이템 개수: " << Items2.size() << endl;

  return 0;
}
결과 

그림2 

[리스트 1]의 Items에서 '긴칼'을 검색을 하면 찾을 수가 없습니다. 이유는 key의 자료형으로 char*을 사용했기 때문입니다. 그래서 Items2에서는 STL의 문자열 라이브러리인 string을 사용하였습니다. String에 대해서는 다음 기회에 설명할 예정이니 문자열을 처리하는 라이브러리라고 알고 계시면 됩니다. 

이것으로 map에 대한 설명이 끝났습니다. 이전 회의 hash_map과 비슷한 부분이 많아서 hash_map에 대한 글을 보셨던 분들은 쉽게 따라왔으리라 생각합니다. 그리고 map과 hash_map에 대하여 잘못 알고 있어서 정렬이 필요하지 않은 곳에서 map을 사용하는 경우가 있는데 조심하시기 바랍니다. 제가 미쳐 설명하지 않은 부분에 대해서는 MSDN에 있는 map 설명을 참조하시기를 바랍니다(http://msdn.microsoft.com/ko-kr/library/xdayte4c.aspx). 

과제 

1. [리스트 1]은 아이템 이름을 key 값으로 사용하였습니다. 이번에는 아이템 가격을 Key 값으로 사용하여 아이템을 저장하고 내림차순으로 출력해 보세요. 

앞서 중복된 key를 사용할 수 있는 multi_map 이라는 것이 있다고 이야기 했습니다. 이번 과제는 multi_map을 사용해야 합니다. multi_map에 관한 설명은 아래 링크의 글을 참조하세요. http://msdn.microsoft.com/ko-kr/library/1y9w8dz4.aspx

반응형
반응형


http://www.hanb.co.kr/network/view.html?bi_id=1617



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

About STL을 보시는 분은 대부분 아직 STL을 잘 모르는 분들이라고 생각합니다. 제가 일하고 있는 게임업계는 주력 언어가 C++입니다. 그래서 취업 사이트에 올라온 프로그래머 채용 공고를 보면 필수 조건에 거의 대부분이 C++와 STL 사용 가능이 들어가 있습니다. 게임 업계뿐 아니라 C++을 사용하여 프로그래밍하는 곳이라면 대부분 C++과 STL을 사용하여 프로그램을 만들 수 있는 실력을 필요로 합니다.
C++ 언어를 배우고 사용하는 프로그래머라면 STL을 배우면 좋고, 특히 게임 프로그래머가 되실 분들은 STL을 꼭 사용할 줄 알아야 됩니다.
작년 여름부터 About STL을 쓰기 시작하여 지금은 2009년이 되었습니다. About STL 집필 계획으로는 이제 반 정도 도달한 것 같습니다. 앞으로 남은 반도 STL을 습득하는데 도움이 되도록 저도 최대한 노력할 테니 2009년에는 STL을 꼭 마스터하기를 바랍니다. 

6.1 시퀸스 컨테이너와 연관 컨테이너 

이전 회까지는 STL의 컨테이너에 대해서 설명했었습니다. STL 컨테이너는 크게 시퀸스 컨테이너와 연관 컨테이너로 나눕니다.
시퀸스 컨테이너는 vector, list, deque와 같이 순서 있게 자료를 보관합니다.
연관 컨테이너는 어떠한 Key와 짝을 이루어 자료를 보관합니다. 그래서 자료를 넣고, 빼고, 찾을 때는 Key가 필요합니다. 

그림1
[그림 1] 시퀸스 컨테이너와 연관 컨테이너 

시퀸스 컨테이너는 많지 않은 자료를 보관하고 검색 속도가 중요한 경우에 사용하고, 연관 컨테이너는 대량의 자료를 보관하고 검색을 빠르게 하고 싶을 때 사용합니다. 제가 만드는 온라인 게임 서버에서는 보통 접속한 유저들의 정보를 보관할 때 가장 많이 사용합니다. 

6.2 연관 컨테이너로는 무엇이 있을까요? 

연관 컨테이너로 map, set, hash_map, hash_set이 있습니다. 이것들은 Key로 사용하는 값이 중복되지 않은 때 사용합니다. 만약 중복되는 key를 사용할 때는 컨테이너의 앞에 'multi'를 붙인 multi_map, multi_set, hash_multimap, hash_multiset을 사용합니다. Key의 중복 허용 여부만 다를 뿐 사용방법은 같습니다. 

6.2.1 map, set 과 hash_map, hash_set의 차이는? 

가장 쉽게 알 수 있는 큰 차이는 이름 앞에 'hash'라는 단어가 있냐 없냐의 차이겠죠.^^
네, 'hash'라는 단어가 정말 큰 차이입니다. 
map과 set 컨테이너는 자료를 정렬하여 저장합니다. 그래서 반복자로 저장된 데이터를 순회할 때 넣은 순서로 순회하지 않고 정렬된 순서대로 순회합니다. hash_map, hash_set은 정렬 하지 않으며 자료를 저장합니다. 또 hash라는 자료구조를 사용함으로 검색 속도가 map, set에 비해 빠릅니다. 

map, set과 hash_map, hash_set 중 어느 것을 사용할지 생각할 때는
map, set의 사용하는 경우 : 정렬된 상태로 자료 저장을 하고 싶을 때.
hash_map, hash_set : 정렬이 필요 없으며 빠른 검색을 원할 때. 

를 가장 큰 조건으로 보면 좋습니다. 

6.2.2 hash_map, hash_set은 표준은 아닙니다. 

위에 열거한 연관 컨테이너 중 map과 set은 STL 표준 컨테이너지만 hash_map, hash_set은 표준이 아닙니다. 그래서 보통 STL 관련 책을 보시면 hash_map과 hash_set에 대한 설명은 없습니다. hash_map, hash_set을 쓰려면 라이브러리를 설치해야 할까요? 그럴 필요는 없습니다. STL 표준은 아니지만 오래되지 않은 C++ 컴파일러에서는 대부분 지원합니다. 윈도우에서는 Visual Studio.NET의 모든 버전에서 지원합니다.
STL 표준도 아닌 hash_map을 설명하려는 이유는 대부분 C++ 컴파일러에서 지원하고, 새로운 C++ 표준에서는 정식으로 STL에 들어갈 예정이며 현업에서 프로그래밍할 때 아주 유용하게 사용하는 컨테이너이기 때문입니다. 

[참고]
2010년 이내에 새로운 C++ 표준이 만들어질 예정인데 표준이 공표되기 전에 TR1으로 일부 공개하고 있습니다. TR1에서는 hash_map, hash_set과 거의 같은 컨테이너인 unordered_map, unordered_set이 준비되어 있습니다. hash_map, hash_set과 이름만 다를 뿐 컨테이너의 자료구조나 사용방법이 거의 같습니다.
그래서 hash_map, hash_set 사용법을 익히며 자동으로 unordered_map, unordered_set도 익히게 됩니다. 

6.3 hash_map의 자료구조 

hash_map의 자료구조는 '해시 테이블'입니다. 
아래의 [그림 2]에 나와 있듯이 해시 테이블에 자료를 저장할 때는 Key 값을 해시함수에 대입하여 버킷 번호가 나오면 그 버킷의 빈 슬롯에 자료를 저장합니다.
Key 값을 해시 함수에 입력하여 나오는 버킷 번호에 자료를 넣으므로 많은 자료를 저장해도 삽입, 삭제, 검색 속도가 거의 일정합니다. 

그림2
[그림 2] 해시 테이블에 자료 넣기 

해시 테이블에 대한 설명은 간단하지 않고 이 글에서는 hash_map 사용법을 간단하게 설명하고 마치려 합니다. 좀 더 자세하게 알고 싶은 분들은 아래의 참고 항목을 꼭 봐 주세요. 

[참고] 해시 테이블 설명
1. http://internet512.chonbuk.ac.kr/datastructure/hash/hash3.htm
2. 좋은 프로그램을 만드는 핵심 원리 25가지(한빛미디어) 

6.4 hash_map을 사용할 때와 사용하지 않을 때 

이전 연재에서 설명한 STL 컨테이너의 장단점은 컨테이너의 자료구조를 보면 알 수 있습니다. hash_map은 해시 테이블을 자료구조로 사용하므로 해시 테이블에 대해 알면 장단점을 파악할 수 있습니다. 해시 테이블은 많은 자료를 저장하고 있어도 검색이 빠릅니다. 그러나 저장한 자료가 적을 때는 메모리 낭비와 검색 시 오버헤드가 생깁니다.
Key 값을 해시 함수에 넣어 알맞은 버킷 번호를 알아 내는 것은 마법 같은 것이 아닙니다. 그러니 hash_map은 해시 테이블을 사용하므로 검색이 빠르다라는 것만 생각하고 무분별하게 hash_map을 사용하면 안됩니다. 컨테이너에 추가나 삭제를 하는 것은 list나 vector, deque가 hash_map보다 빠릅니다. 또 적은 요소를 저장하고 검색할 때는 vector나 list가 훨씬 빠를 수 있습니다. 수천의 자료를 저장하여 검색을 하는 경우에 hash_map을 사용하는 것이 좋습니다. 

hash_map을 사용하는 경우
1. 많은 자료를 저장하고, 검색 속도가 빨라야 한다.
2. 너무 빈번하게 자료를 삽입, 삭제 하지 않는다. 

6.5 Hash_map 사용방법 

STL의 다른 컨테이너와 같이 사용하려면 먼저 헤더 파일과 namespace를 선언해야 합니다. 그러나 여기서 주의할 점은 앞서 이야기 했듯이 hash_map은 표준이 아니므로 표준 STL의 namespace와 다른 이름을 사용하므로 namespace 선언할 때 실수하지 않게 조심하세요.
hash_map 헤더파일을 포함합니다.
#include <hash_map>
hash_map이 속한 namespace는 표준 STL과 다른 'stdext'입니다.
using namespace stdext;
hash_map 선언은 아래와 같습니다.
hash_map< Key 자료 type, Value 자료 type > 변수 이름
위에서는 Value는 저장할 데이터이고, Key는 Value와 가리키는 데이터입니다. 

Key는 int, Value는 float를 사용한다면 아래와 같습니다.
hash_map< int, float > hash1;
다른 컨테이너와 같이 동적 할당을 할 수 있습니다.
hash_map< key 자료 type, Value 자료 type >* 변수 이름 = new hash_map< key 자료 type, Value 자료 type >;
hash_map< int, float >* hash1 = new hash_map< int, float >;
hash_map은 Key와 Value가 짝을 이뤄야 하므로 hash_map을 처음 보는 분들은 이전의 시퀸스 컨테이너와 다르게 좀 복잡하게 보일 것입니다. 그러나 사용이 어려운 것은 아니니 잘 따라와 주세요. 

6.5.1 hash_map의 주요 멤버들 

멤버설명
begin첫 번째 원소의 랜덤 접근 반복자를 반환
clear저장한 모든 원소를 삭제
empty저장한 요소가 없으면 true 반환
end마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase특정 위치의 원소나 지정 범위의 원소들을 삭제
findKey와 연관된 원소의 반복자 반환
insert원소 추가
lower_bound지정한 Key의 요소가 있다면 해당 위치의 반복자를 반환
rbegin역방향으로 첫 번째 원소의 반복자를 반환
rend역방향으로 마지막 원소 다음의 반복자를 반환
size원소의 개수를 반환
upper_bound지정한 Key 요소가 있다면 해당 위치 다음 위치의 반복자 반환


hash_map 컨테이너를 사용할 때는 거의 대부분 추가, 삭제, 검색 이렇게 3가지를 사용합니다. 핵심 기능인 만큼 아래에 좀 더 자세하게 설명하고, 다른 컨테이너는 앞서 설명한 것과 사용방법이 같으므로 예제 코드로 보여 드리겠습니다. 

6.5.2 추가 

insert 

hash_map 에서는 자료를 추가 할 때 insert를 사용합니다.
원형 : 
pair <iterator, bool> insert( const value_type& _Val );
iterator insert( iterator _Where, const value_type& _Val );
template<class InputIterator> void insert( InputIterator _First, InputIterator _Last );
insert를 사용하는 세 가지 방법 중 첫 번째 방식으로 Key 타입은 int, Value 타입은 float를 추가한다면
hash_map<int, float> hashmap1, hashmap2;

// Key는 10, Value는 45.6f를 추가.
hashmap1.insert(hash_map<int, float>::value_type(10, 45.6f));
두 번째 방식으로는 특정 위치에 추가할 수 있습니다.
// 첫 번째 위치에 key 11, Value 50.2f를 추가
hashmap1.insert(hashmap1.begin(), hash_map<int, float>::value_type(11, 50.2f));
세 번째 방식으로는 지정한 반복자 구간에 있는 것들을 추가합니다.
// hashmap1의 모든 요소를 hashmap2에 추가.
hashmap2.insert( hashmap1.begin(), hashmap1.end() );
6.5.3 삭제 

erase
원형 : 
iterator erase( iterator _Where );
iterator erase( iterator _First, iterator _Last );
size_type erase( const key_type& _Key );
첫 번째 방식은 특정 위치에 있는 요소를 삭제합니다.
// 첫 번째 위치의 요소 삭제.
hashmap1.erase( hashmap1.begin() );
두 번째 방식은 지정한 구역에 있는 요소들을 삭제합니다.
// hashmap1의 처음과 마지막에 있는 모든 요소 삭제
hashmap1.erase( hashmap1.begin(), hashmap1.end() );
세 번째 방식은 지정한 키와 같은 요소를 삭제합니다.
// Key가 11인 요소 삭제.
hashmap1.erase( 11 );
첫 번째와 두 번째 방식의 반환 값으로는 삭제된 요소의 다음의 것을 가리키는 반복자이며 세 번째 방식은 삭제된 개수를 반환합니다. 

6.5.4 검색 

hahs_map에서 검색은 Key를 사용하여 같은 Key를 가지고 있는 요소를 찾습니다.
Key와 같은 요소를 찾으면 그 요소의 반복자를 반환하고, 찾지 못한 경우에는 end()를 가리키는 반복자를 반환합니다.
원형 : 
iterator find( const Key& _Key );
const_iterator find( const Key& _Key ) const;
방식은 두 가지지만 사용법은 같습니다. 차이는 반환된 반복자가 const냐 아니냐의 차이입니다. 참고로 첫 번째 방식은 const가 아니므로 찾은 요소의 Value를 변경할 수 있습니다(참고로 Key는 변경 불가입니다). 두 번째 방식은 Value도 변경할 수 없습니다.
// Key가 10인 요소 찾기.
hash_map<int, float>::Iterator FindIter = hashmap1.find( 10 );

// 찾았다면 Value를 290.44로 변경
If( FindIter != hashmap1.end() )
{
   FindIter->second = 290.44f;
}
begin, clear, count, empty, end, rbegin, rend, size는 앞서 말 했듯이 다른 컨테이너와 사용방법이 비슷하므로 아래 예제 코드를 통해서 사용법을 보여 드리겠습니다. 

[리스트 1] hash_map을 사용한 유저 관리
#include <iostream>
#include <hash_map>
using namespace std;
using namespace stdext;

// 게임 캐릭터
struct GameCharacter
{
  // 아래의 인자를 가지는 생성자를 정의한 경우는
  // 꼭 기본 생성자를 정의해야 컨테이너에서 사용할 수 있다.
  GameCharacter() { }

  GameCharacter( int CharCd, int Level, int Money )
  {
    _CharCd = CharCd;
    _Level = Level;
    _Money = Money;
  }
  int _CharCd;    //  캐릭터 코드
  int _Level;    // 레벨
  int _Money;    // 돈
};

void main()
{
  hash_map<int, GameCharacter> Characters;

  GameCharacter Character1(12, 7, 1000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(12, Character1));

  GameCharacter Character2(15, 20, 111000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(15, Character2));

  GameCharacter Character3(200, 34, 3345000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(200, Character3));
  
  // iterator와 begin, end 사용
  // 저장한 요소를 정방향으로 순회
  hash_map<int, GameCharacter>::iterator Iter1;
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl; 

  // rbegin, rend 사용
  // 저장한 요소의 역방향으로순회
  hash_map<int, GameCharacter>::reverse_iterator RIter;
  for( RIter = Characters.rbegin(); RIter != Characters.rend(); ++RIter )
  {
    cout << "캐릭터 코드 : " << RIter->second._CharCd << " |   레벨 : " << RIter->second._Level 
      << "|  가진 돈 : " <<  RIter->second._Money << endl; 
  }
  cout << endl << endl;
 
  // Characters에 저장한 요소 수
  int CharacterCount = Characters.size();

  // 검색. 
  // 캐릭터 코드 15인 캐릭터를 찾는다.
  hash_map<int, GameCharacter>::iterator FindIter = Characters.find(15);
  // 찾지 못했다면 FindIter은 end 위치의 반복자가 반환된다.
  if( Characters.end() == FindIter )
  {
    cout << "캐릭터 코드가 20인 캐릭터가 없습니다" << endl;
  }
  else
  {
    cout << "캐릭터 코드가 15인 캐릭터를 찾았습니다." << endl;
    cout << "캐릭터 코드 : " << FindIter->second._CharCd << " |   레벨 : " << FindIter->second._Level 
      << "|  가진 돈 : " <<  FindIter->second._Money << endl;
  }
  cout << endl;

  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl << endl;
  
  // 삭제
  // 캐릭터 코드가 15인 캐릭터를 삭제한다.
  Characters.erase( 15 );
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl << endl;

  // 모든 캐릭터를 삭제한다.
  Characters.erase( Characters.begin(), Characters.end() );

  // Characters 공백 조사
  if( Characters.empty() )
  {
    cout << "Characters는 비어 있습니다." << endl;
  }
}
결과 

결과1 

6.5.5 lower_bound와 upper_bound 

hash_map에 저장한 요소 중에서 Key 값으로 해당 요소의 시작 위치를 얻을 때 사용하는 멤버들입니다. Key 값의 비교는 크기가 아닌 저장 되어 있는 요소의 순서입니다. 23, 4, 5, 18, 14, 30 이라는 순서로 Key 값을 가진 요소가 저장되어 있으며 Key 값 18과 같거나 큰 것을 찾으면 18, 14, 30이 됩니다. 

lower_bound 

Key가 있다면 해당 위치의 반복자를 반환합니다.
원형 : 
iterator lower_bound( const Key& _Key );
const_iterator lower_bound( const Key& _Key ) const;
upper_bound 

Key가 있다면 그 요소 다음 위치의 반복자를 반환합니다.
원형 : 
iterator lower_bound( const Key& _Key );
const_iterator lower_bound( const Key& _Key ) const;
lower_bound와 upper_bound는 hahs_map에 저장된 요소를 일부분씩 나누어 처리를 할 때 유용합니다. 예를 들면 hash_map에 3,000개의 게임 캐릭터 정보를 저장되어 있으며 이것을 100개씩 나누어서 처리하고 싶을 때 사용하면 좋습니다. 

[리스트 2] lower_bound와 upper_bound 사용 예
#include <iostream>
#include <hash_map>
using namespace std;
using namespace stdext;

// 게임 캐릭터
struct GameCharacter
{
  GameCharacter() { }

  GameCharacter( int CharCd, int Level, int Money )
  {
    _CharCd = CharCd;
    _Level = Level;
    _Money = Money;
  }
  int _CharCd;    //  캐릭터코드
  int _Level;    // 레벨
  int _Money;    // 돈
};

void main()
{
  hash_map<int, GameCharacter> Characters;

  GameCharacter Character1(12, 7, 1000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(12, Character1));

  GameCharacter Character2(15, 20, 111000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(15, Character2));

  GameCharacter Character3(7, 34, 3345000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(7, Character3));

  GameCharacter Character4(14, 12, 112200 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(14, Character4));

  GameCharacter Character5(25, 3, 5000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(25, Character5));

  hash_map<int, GameCharacter>::iterator Iter1;
  cout << "저장한 캐릭터 리스트" << endl;
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl;

  cout << "lower_bound(14)" <<endl;
  hash_map<int, GameCharacter>::iterator Iter = Characters.lower_bound(14);
  while( Iter != Characters.end() )
  {
    cout << "캐릭터 코드 : " << Iter->second._CharCd << " |   레벨 : " << Iter->second._Level 
      << "|  가진 돈 : " <<  Iter->second._Money << endl;

    ++Iter;
  }
  cout << endl;

  cout << "upper_bound(7)" <<endl;
  Iter = Characters.upper_bound(7);
  while( Iter != Characters.end() )
  {
    cout << "캐릭터 코드 : " << Iter->second._CharCd << " |   레벨 : " << Iter->second._Level 
      << "|  가진 돈 : " <<  Iter->second._Money << endl;

    ++Iter;
  }
}
결과 

결과2 

이것으로 hash_map의 주요 사용법에 대한 설명이 끝났습니다.
위에 설명한 것들만 알고 있으면 hash_map을 사용하는데 문제가 없을 것입니다.
위에 설명한 것들 이외의 hash_map의 멤버들에 대해서 알고 싶으며 마이크로소프트의 MSDN 사이트에 있는 것을 참고하세요
http://msdn.microsoft.com/en-us/library/h80zf4bx(VS.80).aspx 

[참고]
Visual C++의 hash_map의 성능에 대해서 Visual C++에 있는 hash_map은 다른 컴파일에서 구현한 것보다 꽤 느리다라는 말이 있습니다.
(관련 글은 여기서 참고하세요. http://minjang.egloos.com/1983788 http://junyoung.tistory.com/1

얼마나 느린지 테스트했습니다.
http://blog.naver.com/jacking75/140062720030
제가 조사한 것은 Windows 플랫폼에서 VC++에서 제공한 라이브러리로 테스트한 것입니다.
결과를 보면 hash_map이 map보다 빠르지도 않고 특히 hash_map과 같은 자료구조를 사용하는 컨테이너로 마이크로소프트사에서 만든 CAtlMap에 비해 속도가 아주 느립니다.
성능이 중요한 곳에 hash_map을 사용한다면 VC++에 있는 것을 사용하지 말고 자체적으로 잘 만들어진 hash 함수를 사용하거나 C++ 오픈 소스 라이브러리인 boost에 있는 unordered_map을 사용하는 것이 좋을 것 같습니다. Windows 플랫폼에서만 사용한다면 CAtlMap을 사용하는 것도 좋습니다.

반응형
반응형

http://www.hanb.co.kr/network/view.html?bi_id=1616


제공 : 한빛 네트워크
저자 : 최흥배

5.5.3. deque 실습 예제

다음은 deque에서 가장 자주 사용하는 멤버들을 사용하는 전체 코드입니다. 

[리스트 1]
#include 
#include 

using namespace std;


// 서버/ 클라이언트간에 주고 받는 패킷
struct Packet
{
	unsigned short Index; // 패킷 인덱스
	unsigned short BodySize; // 패킷 보디(실제 데이터)의 크기
	char	       acBodyData[100]; // 패킷의 데이터
};


int main() 
{
	Packet Pkt1;
	Pkt1.Index = 1;		Pkt1.BodySize = 10;

	Packet Pkt2;
	Pkt2.Index = 2;		Pkt2.BodySize = 12;

	Packet Pkt3;
	Pkt3.Index = 3;		Pkt3.BodySize = 14;


	deque< Packet > ReceivePackets;


	// 뒤에 추가
	ReceivePackets.push_back( Pkt2 );
	ReceivePackets.push_back( Pkt3 );

	// 앞에 추가
	ReceivePackets.push_front( Pkt1 );

	
	// 저장된 패킷 정보 출력
	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index << endl;
		cout << "패킷 바디 크기: " << iterPos->BodySize << endl;
	}

	cout << endl << "역방향으로 출력" << endl;
	for( deque< Packet >::reverse_iterator iterPos = ReceivePackets.rbegin();
		iterPos != ReceivePackets.rend();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index << endl;
		cout << "패킷 바디 크기: " << iterPos->BodySize << endl;
	}

	cout << endl << "배열 방식으로 접근" << endl;
	// 저장된 총 패킷 수
	int ReceivePacketCount = ReceivePackets.size();
	cout << "총 패킷 수: " << ReceivePacketCount << endl;
	for( int i = 0; i < ReceivePacketCount; ++i )
	{
		cout << "패킷 인덱스: " << ReceivePackets[i].Index << endl;
		cout << "패킷 바디 크기: " << ReceivePackets[i].BodySize << endl;
	}


	// 첫 번째, 마지막 위치에 있는 패킷
	Packet& FirstPacket = ReceivePackets.front();
	cout << "첫 번째 패킷의 인덱스: " << FirstPacket.Index << endl;

	Packet& LastPacket = ReceivePackets.back();
	cout << "마지막 패킷의 인덱스: " << LastPacket.Index << endl;

	// at을 사용하여 두 번째 패킷
	Packet& PacketAt = ReceivePackets.at(1);
	cout << "두 번째 패킷의 인덱스: " << PacketAt.Index << endl;


	// 첫 번째 패킷 삭제
	ReceivePackets.pop_front();
	cout << "첫 번째 패킷의 인덱스: " << ReceivePackets[0].Index << endl;

	// 마지막패킷삭제
	ReceivePackets.pop_back();
	LastPacket = ReceivePackets.back();
	cout << "마지막 패킷의 인덱스: " << LastPacket.Index << endl;

	// 모든 패킷을 삭제
	if( false == ReceivePackets.empty() )
	{
		cout << "모든 패킷을 삭제합니다." << endl;
		ReceivePackets.clear();
	}
}
결과 

null

5.5.3.1. insert 멤버

deque의 insert는 vector의 insert와 사용 방법이 같습니다. 지정된 위치에 삽입, 지정된 위치에 지정된 개수만큼 삽입, 지정된 위치에 반복자 구간 안에 있는 것들을 삽입이 있습니다. vector와 같이 insert는 삽입 되는 위치 이후에는 있는 모든 원소들이 뒤로 이동을 합니다. 그리고 insert의 성능은 vector 보다도 더 좋지 않습니다.
원형 : 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 );
 
[그림 6] deque의 insert 처리 과정 

지정한 위치에 데이터 삽입. 아래는 300을 첫 번째 위치에 삽입합니다.
deque< int >::iterator iterInsertPos = deque1.begin();
deque1.insert( iterInsertPos, 300 );
지정한 위치에 데이터를 횟수만큼 삽입. 아래는 두 번째 위치에 10을 3번 추가합니다.
iterInsertPos = deque1.begin();
++iterInsertPos;
deque1.insert( iterInsertPos, 3, 10 );
지정한 위치에 반복자의 시작과 끝 안에 있는 원소를 삽입. 아래는 두 번째 위치에 deque2의 처음과 끝에 있는 원소를 삽입합니다.
deque< int > deque2;
deuqe2.push_back( 20 );
deuqe2.push_back( 30 );
deuqe2.push_back( 40 );

iterInsertPos = deque1.begin();
deque1.insert( ++iterInsertPos, deque2.begin(),deque2.end() );

5.5.3.2. erase 멤버

지정한 위치에 있는 원소를 삭제, 지정한 범위 안에 있는 원소를 삭제합니다. vector와 사용법이 같고 또 erase를 하는 위치 이후의 모든 원소들이 앞으로 이동하는 것도 같습니다.
원형 : iterator erase( iterator _Where );
      iterator erase( iterator _First, iterator _Last );
 
[그림 7] deque의 erase 처리 과정 

지정한 위치의 요소를 삭제. 아래는 첫 번째 요소를 삭제하는 코드입니다.
deque1.erase( deque1.begin() );
지정한 반복자 구간 안의 원소를 삭제합니다. 아래는 deque1의 첫 번째 요소에서 마지막까지 모두 삭제합니다.
deque1.erase( deque1.begin(), deque1.end() );
아래는 insert와 erase 사용 예입니다. 

[리스트 2] Insert와 erase
#include 
#include 

using namespace std;


int main()
{
	Packet Pkt1;
	Pkt1.Index = 1;		Pkt1.BodySize = 10;

	Packet Pkt2;
	Pkt2.Index = 2;		Pkt2.BodySize = 12;

	Packet Pkt3;
	Pkt3.Index = 3;		Pkt3.BodySize = 14;

	Packet Pkt4;
	Pkt4.Index = 4;		Pkt4.BodySize = 16;

	deque< Packet > ReceivePackets;
	ReceivePackets.push_back( Pkt1 );
	ReceivePackets.push_back( Pkt2 );
	ReceivePackets.push_back( Pkt3 );

	cout << "< insert >" << endl;

	// 첫 번째 위치에 Pkt3을 삽입
	cout << "insert 1" << endl;
	ReceivePackets.insert( ReceivePackets.begin(), Pkt3 );

	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index;
		cout << "        패킷 바디 크기: " << iterPos->BodySize << endl;
	}

	// 두 번째 위치에 Pkt4를 2개 삽입
	cout << endl << "insert 2" << endl;
	ReceivePackets.insert( ++ReceivePackets.begin(), 2, Pkt4 );
	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index;
		cout << "        패킷 바디 크기: " << iterPos->BodySize << endl;
	}


	deque< Packet > ReceivePackets2;
	ReceivePackets2.push_back( Pkt3 );
	ReceivePackets2.push_back( Pkt4 );
	ReceivePackets2.push_back( Pkt1 );

	// ReceivePackets2의 모든 것을 ReceivePackets의 첫 번째 위치에 삽입
	cout << endl << "insert 3" << endl;
	ReceivePackets.insert( ReceivePackets.begin(), ReceivePackets2.begin(), 
                                  ReceivePackets2.end() );
	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index;
		cout << "        패킷 바디 크기: " << iterPos->BodySize << endl;
	}


	cout << endl << "< erase >" << endl;	
	// 두 번째 원소를 삭제한다.
	cout << "erase 1" << endl;
	ReceivePackets.erase( ++ReceivePackets.begin() );
	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index;
		cout << "        패킷 바디 크기: " << iterPos->BodySize << endl;
	}

	// 두 번째 이후부터 모두 삭제한다.
	cout << endl << "erase 2" << endl;
	ReceivePackets.erase( ++ReceivePackets.begin(), ReceivePackets.end() );
	for( deque< Packet >::iterator iterPos = ReceivePackets.begin();
		iterPos != ReceivePackets.end();
		++iterPos )
	{
		cout << "패킷 인덱스: " << iterPos->Index;
		cout << "        패킷 바디 크기: " << iterPos->BodySize << endl;
	}
}
결과 

5.5.1.4 assign

vector의 assign과 같이 deque의 assign도 컨테이너를 특정 데이터로 채울 때 사용합니다. 특정 값이나 다른 deque의 특정 영역(반복자로 가리키는)에 있는 데이터로 채울 수 있습니다. assign을 사용하는 deque에 데이터가 있다면 이를 덮어쓰면서 채웁니다.
원형 : void assign( size_type _Count, const Type& _Val );
template void assign( InputIterator _First, InputIterator _Last );
 
[그림 8] deque의 assign 

지정 데이터를 지정 개수만큼 채웁니다. 숫자 7를 7개 채웁니다.
deque1.assign( 7, 7 );
다른 deque의 반복자로 지정한 영역으로 채웁니다.
deque1.erase( deque1.begin(), deque1.end() );

5.5.1.5 swap

deque1과 deque2가 있을 때 두 deque에 저장한 데이터를 서로 맞바꿀 때 사용합니다. swap 원형은 아래와 같습니다.
원형 : void swap( deque& _Right );
friend void swap( deque& _Left, deque& _Right )
template void swap(
       deque< Type, Allocator>& _Left, deque< Type, Allocator>& _Right );
deque A와 B를 swap하는 모습을 그림으로 나타내면 아래와 같습니다. 

 
[그림 9] deque의 swap 

아래는 deque1과 deque2를 swap하는 방법을 두 가지로 나타낸 코드입니다.
deque1.swap( deque2 );
swap(deque1, deque2 );
[리스트 3] assign, swap
#include 
#include 

using namespace std;

int main()
{
	deque< int > deque1;

	cout << "assign 1" << endl;
	deque1.assign( 7, 7 );
	for( deque< int >::iterator iterPos = deque1.begin();
		iterPos != deque1.end();
		++iterPos )
	{
		cout << "deque 1 : " << *iterPos << endl;
	}

	cout << endl << "assign 2" << endl;
	deque< int > deque2;
	deque2.assign( deque1.begin(), deque1.end() );
	for( deque< int >::iterator iterPos = deque2.begin();
		iterPos != deque2.end();
		++iterPos )
	{
		cout << "deque 2 : " << *iterPos << endl;
	}

	// swap
	deque< int > deque3;
	deque3.push_back(10);
	deque3.push_back(20);
	deque3.push_back(30);

	cout << endl << "swap" << endl;
	deque3.swap( deque1 );
	for( deque< int >::iterator iterPos = deque3.begin();
		iterPos != deque3.end();
		++iterPos )
	{
		cout << "deque 3 : " << *iterPos << endl;
	}

	for( deque< int >::iterator iterPos = deque1.begin();
		iterPos != deque1.end();
		++iterPos )
	{
		cout << "deque 1 : " << *iterPos << endl;
	}
}
결과 

 

deque에서 자주 사용하는 deque의 멤버를 중심으로 설명하였습니다. 여기에서 설명하지 않은 나머지 deque 멤버의 사용법은 여기(http://msdn.microsoft.com/en-us/library/8tk0b6f0.aspx)에서 참고해 주세요. 앞에서 여러 번 언급했듯이 deque 사용법이 이전 회의 vector와 거의 같습니다. 위 예제에서 deque를 vector로 바꾸어도 push_front를 제외하고는 문제없이 컴파일할 수 있습니다. deque와 vector는 사용법은 비슷하나 자료구조는 완전히 다릅니다. 앞과 끝에 데이터를 추가, 삭제하는 일이 대부분이라면 deque가 vector보다 좋지만, 그렇지 않다면 vector를 사용하는 쪽이 훨씬 더 좋습니다. STL 컨테이너는 사용이 어렵다기보다는 언제 어떤 STL 컨테이너를 사용해야 알맞은지 선택하기가 어렵습니다. STL 컨테이너를 올바른 장소에 사용하려면 컨테이너의 자료구조가 무엇인지 확실하게 알고 있어야 합니다. 만약 자료구조를 잘 모른다면 꼭 자료구조 공부를 STL 공부보다 먼저 해야 합니다.

5.6. 과제

1. deque를 사용하여 FIFO, LIFO 방식의 스택을 만들어 보세요. 

2. deque를 사용하여 Undo, Redo 구현해 보세요. 
간단한 예제로 deque의 원리만 익혀보는 과제입니다. Deque에 숫자를 저장하고 Undo를 하면 가장 마지막에 넣은 데이터를 빼고 Redo를 하면 뺀 데이터를 다시 넣습니다. deque를 2개 사용하면 횟수 제한이 없는 Undo, Redo를 구현할 수 있습니다.

반응형
반응형

제공 : 한빛 네트워크
저자 : 최흥배

이번 회는 STL 컨테이너 라이브러리 중 하나인 deque를 설명합니다. 앞 회의 list, vector 글을 보신 분들은 아시겠지만 STL 컨테이너 라이브러리는 사용하는 방법이 서로 비슷하므로 하나만 잘 알면 다른 컨테이너도 쉽게 배울 수 있습니다. 이전에 연재했던 list나 vector에 대한 글을 보지 않은 분은 꼭 보신 후 이 글을 보기를 권합니다. 또 이미 list나 vector를 알고 있는 분들은 deque의 자료구조 및 특징을 잘 파악하기를 바랍니다. 

5.1 deque의 자료구조

deque의 자료구조는 이름과 같이 Deque(Double Ended Queue) 자료구조입니다. Deque 자료구조는 Queue 자료구조와 비슷하므로 먼저 Queue 자료구조를 설명하겠습니다. Queue는 선형 리스트로 선입선출(FIFO) 방식을 사용합니다. 아래 그림처럼 시작과 끝을 가리키는 포인터로 삽입과 삭제를 합니다. 

null 
[그림 1] 큐(Queue) 자료구조 

[그림 1]에서 F(Front)는 가장 앞에 있는 것을 가리키며 삭제 작업을 할 때 사용합니다. 위 그림에서 삭제를 한다며 ‘a’는 없어지고 F는 ‘b’를 가리킵니다. R(Rear)은 가장 마지막에 있는 것을 가리키며 삽입 작업을 할 때 사용합니다. 위 그림에서 ‘g’를 추가하면 ‘f’ 다음에 위치하며 R은 ‘f’를 가리킵니다.
Queue는 앞으로는 삭제, 뒤로는 삽입을 할 때 사용합니다.
OS의 작업 스케줄링처럼 입력 순서대로 처리를 할 때 Queue를 사용하면 좋습니다. Deque과 Queue와 다른 점은 삽입과 삭제를 한쪽이 아닌 앞, 뒤 양쪽에서 할 수 있다는 것만 다르며 Queue와 같습니다. 

 
[그림 2] 덱(Deque) 자료구조 

[그림 1]과 다르게 [그림 2]를 보면 앞과 뒤에서 삽입과 삭제를 할 수 있습니다. Deque는 Stack과 Queue의 장점을 모은 것으로 FIFO 방식과 LIFO 방식 둘 다 사용할 수 있습니다. 

5.2 Deque의 특징

Deque의 장점을 정리하면 아래와 같습니다. 

1. 크기가 가변적이다. 
리스트와 같이 데이터를 담을 수 있는 크기가 가변적입니다. 

2. 앞과 뒤에서 삽입과 삭제가 좋다. 
Deque가 다른 자료구조와 가장 다른 점으로 앞과 뒤에서 삽입, 삭제가 좋습니다. 

3. 중간에 데이터 삽입, 삭제가 용이하지 않다. 
데이터를 중간에 삽입하거나 삭제하는 것은 피해야 합니다. 삽입과 삭제를 중간에 한다면 삽입하거나 삭제한 위치 앞뒤 데이터를 모두 이동해야 합니다. 

 
[그림 3] 데이터 g를 중간에 삽입하는 과정 

 
[그림 4] 데이터 c를 삭제하는 과정 

4. 구현이 쉽지 않다. 
Deque은 Stack과 Queue가 결합된 자료구조로 연결 리스트보다 구현하기가 더 어렵습니다. 

5. 랜덤 접근이 가능하다. 

연결 리스트처럼 리스트를 탐색하지 않고 원하는 요소에 바로 접근할 수 있습니다.

5.3 deque를 사용하는 경우

Deque의 특징을 고려할 때 다음과 같은 경우에 사용하면 좋습니다. 

1. 앞과 뒤에서 삽입, 삭제를 한다. 
이것이 deque를 사용하는 가장 큰 이유입니다. 대부분 작업이 데이터를 앞이나 뒤에 삽입, 삭제를 한다면 STL 컨테이너 라이브러리 중에서 deque를 사용할 때 성능이 가장 좋습니다. 

2. 저장할 데이터 개수가 가변적이다. 
저장할 데이터 개수를 미리 알 수 없어도 deque는 크기가 동적으로 변하므로 유연하게 사용할 수 있습니다. 

3. 검색을 거의 하지 않는다. 
많은 데이터를 저장한다면 앞 회에서 여러 번 언급했듯이 map, set, hash_map 중 하나를 선택해서 사용하는 편이 좋습니다. 

4. 데이터 접근을 랜덤하게 하고 싶다. 
vector와 같이 랜덤 접근이 가능합니다. 사용하는 방법도 같습니다. 

5.4 deque VS vector

deque는 전체적으로 멤버 함수의 기능이나 사용 방법이 vector와 거의 같습니다. vector는 삽입과 삭제를 뒤(back)에서만 해야 성능이 좋지만, deque는 삽입과 삭제를 앞과 뒤에서 해도 좋으며 앞뒤 삽입, 삭제 성능도 vector보다 좋습니다. 하지만, deque는 앞뒤에서 삽입, 삭제하는 것을 제외한 다른 위치에서의 삽입과 삭제는 vector보다 성능이 좋지 않습니다. 
 dequevector
크기 변경 가능OO
앞에 삽입, 삭제 용이OX
뒤에 삽입, 삭제 용이OO
중간 삽입, 삭제 용이XX
순차 접근 가능OO
랜덤 접근 가능OX
[표 1] deque와 vector의 차이 

deque와 vector를 비교할 때 고려해야 되는 점은 
deque는 앞과 뒤에서 삽입과 삭제 성능이 vector보다 더 좋다. 
deque는 앞과 뒤 삽입, 삭제를 제외한 기능은 vector보다 성능이 좋지 못하다. 

게임 서버에서 deque를 사용하는 경우 

게임 서버는 클라이언트에서 보낸 패킷을 차례대로 처리합니다. 서버에서 네트워크 데이터를 받는 함수에서 데이터를 받으면 패킷으로 만든 후 받은 순서대로 순차적으로 처리합니다. 이렇게 순차적으로 저장한 패킷을 처리할 때는 deque가 가장 적합한 자료구조입니다. 다만, 실제 현업에서는 이 부분에 STL의 deque를 사용하지 않는 경우가 종종 있습니다. 이유는 네트워크에서 데이터를 받아 패킷으로 만들어 저장하고, 그 패킷을 처리하는 부분은 게임 서버의 성능 면에서 가장 중요한 부분이므로 deque보다 더 빠르게 처리하기를 원하므로 독자적인 자료구조를 만들어 사용합니다(즉, 범용성보다는 성능을 우선시합니다). 

5.5 deque 사용 방법

deque를 사용하려면 deque 헤더 파일을 포함합니다.
#include 
deque 형식은 아래와 같습니다.
deque< 자료 type > 변수 이름
int를 사용하는 deque 선언은 아래와 같습니다.
deque< int > deque1;
deque도 동적 할당을 할 수 있습니다.
deque< 자료 type >* 변수 이름 = new deque< 자료 type >;
deque< int >* deque1 = new deque< int >;

5.5.1 deque의 주요 멤버들

deque 멤버 중 일반적으로 자주 사용하는 멤버들입니다. vector에 없는 pop_front와 push_front가 있다는 것 빼고는 vector의 기능과 같습니다. 

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

5.5.2. 기본 사용 멤버

deque의 가장 기본적인(추가, 삭제, 접근 등) 사용법을 설명합니다. 
멤버원형설명
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();마지막 원소를 삭제
pop_frontvoid pop_front( );첫 번째 원소를 삭제
push_backvoid push_back( const Type& _Val );마지막에 원소를 추가
push_frontvoid push_front( const Type& _Val );제일 앞에 원소 추가
rbeginreverse_iterator rbegin( );
const_reverse_iterator rbegin( ) const;
역방향으로 첫 번째 원소의 반복자를 반환
rendconst_reverse_iterator rend( ) const;
reverse_iterator rend( );
역방향으로 마지막 원소 다음의 반복자를 반환
sizesize_type size() const;원소의 개수를 반환
[표 3] 추가, 삭제, 접근 등에 사용하는 멤버들 

 
[그림 5] deque의 추가, 삭제, 접근 멤버들 

추가 

앞과 뒤에 추가를 할 수 있습니다. 보통 deque를 사용할 때는 뒤에 추가를 하고 앞에서는 삭제를 합니다. 앞쪽 추가는 push_front()를 사용합니다.
deque< int > deque1;
deque1.push_front( 100 );
뒤에 추가할 때는 push_back()을 사용합니다.
deque< int > deque1;
deque1.push_back( 200 );
삭제 

앞과 뒤에서 삭제 할 수 있습니다. 앞에서 삭제를 할 때는 pop_front()를 사용합니다.
deque1.pop_front();
뒤에서 삭제를 할 때는 pop_back()를 사용합니다.
deque1.pop_back();
접근 

첫 번째 위치의 반복자를 얻을 때는 begin()을 사용합니다.
deque< int >::iterator IterBegin = deque1.begin();
cout << *IterBegin << endl;
반복자로 다른 원소에 접근을 할 때는 반복자에 ‘++’ 이나 ‘–-‘을 사용합니다.
deque< int >::iterator IterPos = deque1.begin();

// 두 번째 위치로 이동
++IterPos;
// 첫 번째 위치로 이동
--IterPos;
end()는 deque에 저장된 원소 중 마지막 다음 위치, 즉 사용하지 못하는 영역을 가리킵니다. 보통 반복문에서 컨테이너에 남은 원소가 있는지 조사할 때 주로 사용합니다.
deque< int >::iterator IterEnd = deque1.end();
for(deque< int >::iterator IterPos = deque1.begin; IterPos != IterEnd;
     ++IterPos )
{
   ……
}
첫 번째 위치에 있는 데이터를 얻을 때는 front(), 마지막 위치에 있는 데이터를 얻을 때는 back()을 사용합니다.
int& FirstValue = deque1.front();
int& LastValue = deque1.back();
begin()과 end()는 순방향으로 앞과 뒤를 가리키고, 역방향으로는 rbegin()과 rend()를 사용합니다. 특정 위치에 있는 데이터를 얻을 때는 at()이나 배열 식 접근([])을 사용합니다.
int& Value2 = deque1.at(1); // 두 번째 위치
int Value3 = deque1[2];      // 세 번째 위치
모두 삭제 

clear()를 사용하면 저장한 모든 데이터를 삭제합니다.
deque1.clear();
데이터 저장 여부 

deque에 저장한 데이터가 있는지 없는지는 empty()로 조사합니다. 데이터가 있으면 false, 없다면 true를 반환합니다.
bool bEmpty = deque1.empty();
저장된 원소 개수 조사 

size()로 deque에 저장된 데이터 개수를 조사합니다.
deque< int >::size_type TotalCount = deque1.size();
지금까지 설명한 deque 멤버들의 사용법을 보면 전 회에 설명한 vector와 같다는 것을 충분히 알 수 있을 것입니다. vector를 공부하신 분들은 아주 쉽죠? ^^ 이후에 소개하는 내용도 vector에서 설명한 것과 같으니 편안하게 잘 따라와 주세요.

반응형
반응형

http://www.hanb.co.kr/network/view.html?bi_id=1606



제공 : 한빛 네트워크
저자 : 최흥배

이번 회는 이전 회에 설명한 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/network/view.html?bi_id=1585


제공 : 한빛 네트워크
저자 : 최흥배

이번 회부터는 본격적으로 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/network/view.html?bi_id=1572


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

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

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

경험치 변경 이력 저장

기획팀에서 유저들이 게임에 접속하여 다른 유저들과 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/network/view.html?bi_id=1567



제공: 한빛 네트워크
저자: 최흥배

함수 템플릿

두 값을 비교하는 함수를 만들어야 됩니다.

앞서 제가 하는 일을 이야기했습니다. 네, 온라인 게임을 만들고 있습니다. 게임에서 구현해야 되는 것에는 캐릭터 간에 HP를 비교하는 것이 필요합니다. 그래서 두 개의 int 타입을 비교하는 Max라는 이름의 함수를 하나 만들었습니다.
int Max( int a, int b );
일을 다 끝낸 후 다음 기획서를 보니 캐릭터와 NPC가 전투를 하는 것을 구현해야 되는데 여기에는 경험치를 비교하는 기능이 필요합니다. 구현해야 되는 것은 위에서 만든 Max 함수와 같습니다. 그래서 그것을 사용하였습니다. 

< List 1 >
#include 
using namespace std;

int Max( int a, int b )
{
  return a > b ? a : b;
}

void main()
{
  int Char1_HP = 300;
  int Char2_HP = 400;
  int MaxCharHP = Max( Char1_HP, Char2_HP );
  cout << "HP 중 가장 큰 값은" << MaxCharHP << "입니다." << endl << endl;
  
  float Char1_Exp = 250.0f;
  float Char2_Exp = 250.57f;
  float MaxCharExp = Max( Char1_Exp, Char2_Exp );
  cout << "경험치 중 가장 큰 값은" << MaxCharExp << "입니다." << endl << endl;
}
앗, 체력(HP)을 저장하는 변수의 타입은 int인데, 경험치를 저장하는 변수의 타입은 int가 아닌 float 타입니다. 

그림1 

당연하게 경험치를 비교하는 부분은 버그가 있습니다.
앞에 만들었던 Max와는 다르게 비교하는 변수의 타입이 float인 것이 필요하여 새로 만들었습니다. 

< List 2 >
float Max( float a, float b )
{
  return a > b ? a : b;
}
함수 오버로딩에 의해 경험치를 비교할 때는 int 타입의 Max가 아닌 의 float 타입을 비교하는 Max가 호출되어 버그가 사라지게 되었습니다. 

이제 경험치 비교는 끝나서 다음 기획서에 있는 것을 구현해야 합니다. 이번에는 돈을 비교하는 것이 있습니다. 그런데 돈을 저장하는 변수의 타입은 __int64입니다. __int64는 비주얼 C++에서만 사용할 수 있는 64비트 정수 타입입니다. __int64 타입을 비교하는 것은 앞에서 만든 int 타입의 Max나 float 타입의 Max로 할 수 없습니다. 함수에서 사용하는 변수의 타입만 다를 뿐 똑같은 것을 또 만들어야 됩니다.
__int64 Max(__int64 a, __int64 b )
{
  return a > b ? a : b;
}
현재까지만 하더라도 이미 똑같은 로직으로 구현된 함수를 3개나 만들었는데, 게임에서 사용하는 캐릭터의 정보는 HP, 경험치, 돈 이외에도 더 많습니다. 저는 앞으로 Max 함수를 몇 개 더 만들어야 할지 모릅니다. Max 함수의 구현을 고쳐야 한다면 모든 Max 함수를 찾아야 합니다. 함수 오버로딩은 문제를 해결하지만, 코드가 커지고 유지보수는 어렵게 만듭니다. 

프로그래밍에서 유지보수는 아주 중요합니다. 왜냐하면, 프로그래밍은 언제나 변경이 가해지기 때문입니다. 유지보수를 편하게 하는 가장 간단한 방법은 유지보수 할 것을 줄이는 것입니다.

Max 함수를 하나로 만들고 싶습니다. 어떻게 해야 될까요?

앗, 혹시 모른다고요? 제가 이 앞에 템플릿에 대해 설명을 할 때 이런 말을 하지 않았나요?
'템플릿을 사용하면 타입에 제약을 받지 않는 로직을 기술 할 수 있습니다'
네, 템플릿을 사용하면 됩니다. 

함수 템플릿 Max를 만들자 

아래의 코드는 템플릿을 사용하여 Max 함수를 구현 한 것입니다. 

< List 3 >
#include 
using namespace std;

// 템플릿으로 만든 값을 비교하는 Max 함수
template 
T Max(T a, T b )
{
  return a > b ? a : b;
}


void main()
{
  int Char1_HP = 300;
  int Char2_HP = 400;
  int MaxCharHP = Max( Char1_HP, Char2_HP );
  cout << "HP 중 가장 큰 값은" << MaxCharHP << "입니다." << endl << endl;
  
  float Char1_Exp = 250.0f;
  float Char2_Exp = 250.57f;
  float MaxCharExp = Max( Char1_Exp, Char2_Exp );
  cout << "경험치 중 가장 큰 값은" << MaxCharExp << "입니다." << endl << endl;
}
실행한 결과는 다음과 같습니다. 

그림2 

네 이번에는 경험치 비교가 정확하게 이루어졌습니다.
템플릿을 사용하게 되어 이제는 불필요한 Max 함수를 만들지 않아도 됩니다.. 

List 3 코드에서 template으로 만든 함수를 '함수 템플릿'이라고 합니다. 

함수 템플릿을 정의하는 방법은 아래와 같습니다. 

그림3 

템플릿을 사용하면 Generic Programming을 할 수 있다
라고 앞서 이야기 했는데 위의 Max 함수 템플릿을 보고 좀 이해를 하셨나요?
혹시나 해서 그림으로 조금만 더 설명하겠습니다. 

그림4 

암소를 총칭(Generic)화하면 동물이라고 할 수 있습니다.
Max 함수 템플릿에서는 함수의 반환 값과 함수 인자인 a 와 b의 타입인 int 나 float를 T로 Generic화 하였습니다. 

그림5

함수 템플릿과 컴파일

하나의 Max 함수 템플릿을 만들었는데 어떻게 int 타입의 Max와 float 타입의 Max를 사용할 수 있을까요? 비밀은 컴파일하는 과정에 있습니다. 컴파일할 때 템플릿으로 만든 것은 템플릿으로 만든 함수를 호출하는 부분에서 평가합니다. 가상 함수처럼 실행시간에 평가하는 것이 아닙니다. 

컴파일을 할 때(compile time) 함수 템플릿을 평가하므로 프로그램의 성능에 해가 되는 것은 없습니다. 

컴파일할 때 평가를 하면서 문법적으로 에러가 없는지 검사합니다. 만약 에러가 있다면 컴파일 에러를 출력합니다. 에러가 없다면 관련 코드를 내부적으로 생성합니다. 

List 3을 예로 들면, void main()의 다음 부분을 컴파일하면 Max를 호출할 때 사용한 인자의 변수의 타입이 Max에서 정의 한 문법에 틀리지 않는지 체크한 후 int 타입을 사용하는 Max 함수의 코드를 만듭니다.
int MaxCharHP = Max( Char1_HP, Char2_HP );
이후 다음 부분에서 Max를 만나면 이번에도 위의 int 때와 같이 문법 체크를 한 후 에러가 없다면 float를 사용하는 Max 함수 코드를 만듭니다.
float MaxCharExp = Max( Char1_Exp, Char2_Exp );
Max가 만들어지는 과정을 나타내면 아래와 같습니다. 모든 타입에 대해 Max 함수를 만드는 것은 아닙니다. 코드에서 사용한 타입에 대해서만 Max 함수가 만들어집니다. 

그림6 

참고로 이렇게 만들어지는 코드는 소스 코드에 만들어지는 것이 아니고 프로그램의 코드 영역에 만들어집니다. 컴파일 타임에 함수 템플릿을 평가하고 관련 코드를 만들기 때문에 템플릿을 많이 사용하면 컴파일 시간이 길어질 수 있으며, 각 타입에 맞는 코드를 만들어내므로 실행 파일의 크기도 커질 수 있습니다.

Max 함수 템플릿에 개선점이 없을까요?

힌트를 드린다면 Max의 두 인자 값은 함수 내부에서 변경되지 않습니다. 그리고 인자의 타입은 C++의 기본형뿐만이 아닌 크기가 큰 타입을 사용할 수도 있습니다. 

생각나셨나요? C++ 기초 공부를 차근차근 쌓아 올린 분이라면 알아차렸으리라 생각합니다. 

정답은 Max 함수 템플릿을 만들 때 템플릿의 인자에 const와 참조를 사용하는 것입니다. Max 함수는 함수의 내부에서 함수의 인자를 변경하지 않습니다. 그러니 함수에 const를 사용하여 내부에서 변경하는 것을 명시적으로 막고 Max 함수를 사용하는 사람에게 알리는 역할을 합니다. 

C++에서 함수 인자의 전달을 빠르게 하는 방법은 참조로 전달하는 것입니다. 위의 Max 함수는 int나 float 같은 크기가 작은 타입을 사용하였기 때문에 참조로 전달하는 것이 큰 의미는 없지만, 만약 구조체나 클래스로 만들어진 크기가 큰 변수를 사용할 때는 참조로 전달하는 것이 훨씬 빠릅니다. 앞에 만든 Max 함수 템플릿을 const와 참조를 사용하는 것으로 바꾸어 보았습니다. 

< List 4 >
template 
const T& Max(const T& a, const T& b )
{
  return a > b ? a : b;
}

class T 라는 것을 본적이 있나요?

함수 템플릿을 만들 때 'typename'을 사용했습니다. 그러나 좀 오래된 C++ 책에서 템플릿에 대한 글을 본 적이 있는 분은 'class'를 사용한 것도 본 적이 있을 것입니다. 

< List 5 >
template 
const T& Max(const T& a, const T& b )
{
  return a > b ? a : b;
}
typename과 class는 기능적으로 다른 것이 아닙니다. 템플릿이 표준이 되기 전에는 'class'를 사용했습니다. 그래서 표준화 이전이나 조금 지난 뒤에 나온 책에서는 'class'로 표기했습니다. 그리고 예전에 만들어진 C++ 컴파일러도 템플릿 인자 선언으로 'class'만 지원했습니다. 만약, C++ 표준화 전후에 만들어진 컴파일러에서는 'class'를 사용해야 합니다. 

현재의 컴파일러에서는 'class', 'typename' 둘 다 지원합니다. 하지만, 'class'보다 프로그래머에게 '타입'을 추상화한 것이라는 의미 전달을 명확하게 하는 typename을 사용합니다. class만 지원하는 오래된 C++ 컴파일러에서 컴파일 해야 하는 것이 아니면 꼭 'typename'을 사용하세요.

이제 Max 함수 템플릿에는 문제가 없을까요?

위에서 Max 함수 템플릿에 대해서 const와 참조로 개선을 했는데 이제 문제가 없을까요? 

그럼 아래의 코드는 문제가 없이 컴파일이 잘 될까요? 

< List 6 >
// List3의 Max 함수 템플릿을 사용합니다.
void main()
{
  int Char1_MP = 300;
  double Char1_SP = 400.25;
  double MaxValue1 = Max( Char1_MP, Char1_SP );
  cout << "MP와 SP 중 가장 큰값은" << MaxValue1 << "입니다." << endl << endl;
  
  double MaxValue2 = Max( Char1_SP, Char1_MP );
  cout << "MP와 SP 중 가장 큰값은" << MaxValue2 << "입니다." << endl << endl;
}
List 6을 컴파일 하면 다음과 같은 에러가 출력됩니다.
max.cpp
max.cpp(16) : error C2782: 'const T &Max(const T &,const T &)' 
        : 템플릿 매개 변수 'T'이(가) 모호합니다.
        max.cpp(6) : 'Max' 선언을 참조하십시오.
        'double'일 수 있습니다.
        또는      'int'
max.cpp(19) : error C2782: 'const T &Max(const T &,const T &)' 
        : 템플릿 매개 변수 'T'이(가) 모호합니다.
        max.cpp(6) : 'Max' 선언을 참조하십시오.
        'int'일 수 있습니다.
        또는      'double'
이유는 컴파일러는 사람이 아니어서 서로 다른 타입의 인자가 들어오면 템플릿의 파라메터 T를 사용한 함수의 인자 a와 b의 타입을 int로 해야 할지, double로 해야 할지 판단할 수가 없기 때문입니다. 이 문제는 어떻게 해결 해야 될까요?

typename을 하나가 아닌 복수 개 사용하면 됩니다.

위의 문제는 Max 함수를 정의할 때 typename을 하나만 사용해서 타입을 하나만 선언했습니다. 이제 typename을 여러 개 사용하면 위의 문제를 풀 수 있습니다. 

< List 7 >
template 
const T1& Max(const T1& a, const T2& b )
{
  return a > b ? a : b;
}
List 7의 함수 템플릿을 사용하면 Max 함수의 인자 타입을 int와 double 혹은 double과 int 타입을 사용해도 컴파일이 잘 됩니다. 그럼 제대로 실행 되는지 실행을 해 볼까요? 

그림7 

앗, 실행 결과에 오류가 있습니다.
int Char1_MP = 300;
double Char1_SP = 400.25;
double MaxValue1 = Max( Char1_MP, Char1_SP );
이 코드는 300과 400.25를 비교합니다. 결과는 400.25가 나와야 하는데 400이 나와버렸습니다. 

이유는 List 7의 함수 템플릿의 반환 값으로 T1을 선언했기 때문에 int 타입과 double 타입을 순서대로 함수 인자에 사용하면 반환 값의 타입이 int형으로 되어 버리기 때문입니다. 이렇게 서로 다른 타입을 사용하는 경우에는 반환 값을 아주 조심해야 합니다. 그리고 위의 예에서는 함수 템플릿의 파라메터로 typename을 2개 사용했지만 그 이상도 사용할 수 있습니다. 

위의 Max 함수 템플릿 만족스럽나요?
저는 왠지 아직도 좀 불 만족스럽습니다.
Max( int, double);
실수를 하면 찾기 힘든 버그가 발생할 확률이 높습니다.
이것을 어떻게 풀어야 될까요?

함수 템플릿의 전문화 라는 것이 있습니다.

Max(int, double)을 사용하면 Max 함수 템플릿이 아닌 이것에 맞는, 특별하게 만든 함수를 사용하도록 합니다. 함수 템플릿의 전문화(Specialization)라는 특별한 상황에 맞는 함수를 만들면 함수 오버로드와 같이 컴파일러가 상황에 맞는 함수를 선택하도록 합니다. 

< List 8 >
#include 
using namespace std;

// 템플릿으로만든값을비교하는Max 함수
template 
const T1& Max(const T1& a, const T2& b )
{
  cout << "Max(const T& a, const T& b) 템플릿 버전 사용" << endl;
  return a > b ? a : b;
}

// 전문화시킨Max 함수
template <> 
const double& Max(const double& a, const double& b)
{
  cout << "Max(const double& a, const double& b) 전문화 버전 사용" << endl;
  return a > b ? a : b;
}

void main()
{
  double Char1_MP = 300;
  double Char1_SP = 400.25;
  double MaxValue1 = Max( Char1_MP, Char1_SP );
  cout << "MP와 SP 중 가장 큰 값은" << MaxValue1 << "입니다." << endl << endl;
  
  int Char2_MP = 300;
  double Char2_SP = 400.25; 
  double MaxValue2 = Max( Char2_MP, Char2_SP );
  cout << "MP와 SP 중 가장 큰 값은" << MaxValue2 << "입니다." << endl << endl;
}
위 코드를 실행한 결과는 아래와 같습니다. 

그림8 

컴파일러는 프로그래머의 생각을 완전히 이해하지는 않습니다. 그래서 컴파일러가 어떠한 것을 선택할지 이해하고 있어야 됩니다. List 8은 double에 전문화 된 Max 함수를 만든 예입니다. 

[질문] Max(10.1, 20.4)를 호출한다면 Max(T, T)가 호출 될까요? 아님 Max(double, double)가 호출 될까요? 

답을 빨리 알고 싶을 테니 뜸 들이지 않고 결과를 바로 보여드리겠습니다. 

그림9 

전문화 버전이 호출 되었습니다. 이유는 호출 순서에 규칙이 있기 때문입니다(최선에서 최악으로). 호출 순서는 다음과 같습니다.
  1. 전문화된 함수와 맞는지 검사한다.
  2. 템플릿 함수와 맞는지 검사한다.
  3. 일반 함수와 맞는지 검사한다.
위의 순서를 잘 기억하고 전문화 함수를 만들어야 합니다. 잘못하면 찾기 힘든 버그를 만들 수가 있습니다. 이제 함수 템플릿에 대한 이야기는 거의 다 끝난 것 같습니다. 

아... 하나 더 있습니다. 

이때까지 한 것들은 타입만을 템플릿 파라메터로 사용했는데 꼭 타입만 함수 템플릿에 사용할 수 있는 것은 아닙니다.

난-타입(non-type) 함수 템플릿

온라인 게임에서는 특정한 이벤트가 있을 때는 캐릭터의 HP, 경험치, 돈을 이벤트 기념으로 주는 경우가 있습니다. HP와 경험치, 돈의 타입은 다르지만 추가 되는 값은 int 상수로 정해져 있습니다. 위와 같이 타입은 다르지만 상수를 더 한 값을 얻는 함수를 만들려면 어떻게 해야 될까요? 

이런 문제도 함수 템플릿으로 해결할 수 있습니다. 

함수 템플릿의 파라메터로 꼭 typename만이 아닌 값을 파라메터로 사용할 수도 있습니다. 

아래의 코드는 캐릭터의 HP, 경험치, 돈을 이벤트에서 정해진 값만큼 더 해주는 것을 보여줍니다. 

< List 9 >
#include <iostream>
using namespace std;

// 지정된 값만큼 더해준다.
template <typename T, int VAL>
T AddValue( T const& CurValue)
{
  return CurValue + VAL;
}

const int EVENT_ADD_HP_VALUE  = 50;    // 이벤트에 의해 추가 될 HP 값
const int EVENT_ADD_EXP_VALUE  = 30;    // 이벤트에 의해 추가 될 경험치
const int EVENT_ADD_MONEY_VALUE  = 10000;    // 이벤트에 의해 추가 될 돈

void main()
{
  int Char_HP = 250;
  cout << Char_HP <<"에서 이벤트에 의해" << AddValue<int, 
       EVENT_ADD_HP_VALUE>(Char_HP) << " 로 변경" <<endl;

  float Char_EXP = 378.89f;
  cout << Char_EXP <<"에서 이벤트에 의해" << AddValue<float, 
       EVENT_ADD_EXP_VALUE>(Char_EXP) << " 로 변경" <<endl;

  __int64 Char_MONEY = 34567890;
  cout << Char_MONEY <<"에서 이벤트에 의해" << AddValue<__int64,   
       EVENT_ADD_MONEY_VALUE>(Char_MONEY) << " 로 변경" <<endl;
}
실행 결과는 다음과 같습니다. 

그림10 

앞에서 사용했던 함수 템플릿 사용 방법과 좀 틀려서 생소할 수도 있겠네요. 

제가 위에 예로든 것은 난 타입 함수 템플릿을 사용해야 되는 당위성이 좀 떨어질 수도 있다고 생각합니다만 설명을 위해서 간단하게 예를 보여주기 위해서 라고 변명해 봅니다. ^^;; 

난 타입을 사용하는 템플릿은 다음 회에 이야기 할 클래스 템플릿에서도 또 다시 이야기 할 예정이니 잘 기억하고 있으시기를 바랍니다. 또 난 타입을 잘 사용하면 템플릿 메타 프로그래밍을 할 때 큰 도움이 됩니다. 템플릿 메타 프로그래밍에 대해서는 다음에 설명해 드리겠습니다.

반응형
반응형

http://www.hanb.co.kr/network/view.html?bi_id=1563


제공: 한빛 네트워크
저자: 최흥배

STL이 무엇인지 알고 계십니까?

C++를 주 프로그래밍 언어로 사용하고 계신 분들은 알고 있으리라 생각합니다. STL은 C++ 언어의 '표준 템플릿 라이브러리 (Standard Template Library) '의 약자입니다. 

STL을 간단하게 말하자면 일반적으로 많이 사용될 수 있는 자료구조와 알고리즘 모음 라이브러리가 말할 수 있습니다. 

STL은 C++ 언어가 처음 만들어질 때부터 있었던 것이 아니고 1998년에 C++ 표준이 정해지기 전인 1993년 말 무렵에 Alex Stepanov가 C++ 언어의 창시자인 Bjarne Stroustrup에게 보여준 후 준비 기간을 걸쳐서 1994년에 표준 위원회에 초안이 통과됩니다. 

참고로 C++ 표준은 1989년에 시작되어 1998년 9월에 마무리되었습니다.

STL은 어떻게 만들었을까요?

답은 위에 STL의 실제 이름에 포함 되어 있습니다. 좀 더 힌트를 드린다면 세 개의 단어 중 중간에 있는 것입니다. 

네, 템플릿(Template)으로 만들어 진 것입니다. 

STL을 이해하려면 STL을 만들 수 있게 해준 C++의 템플릿에 대한 이해는 필수입니다. 또, 템플릿은 C++를 더욱 강력하게 사용하는 데 꼭 필요합니다.

C++ 언어를 공부한 사람은 템플릿에 대해 잘 알고 있을까요?

예전에 C++을 잠시 공부했던 분이나 근래에 공부하고 있는 분 중 아직 C++ 책을 한, 두 권 본 정도라면 템플릿이라는 단어가 생소할 수 있습니다. 

위에 언급했듯이 템플릿은 C++이 세상에 나오면서 같이 나온 것이 아니고 1994년 무렵에야 세상에 조금씩 소개되다가 1998년에 C++ 표준이 정립되고서야 C++ 언어의 한 부분으로서 인정되었습니다. 

1994년까지는 템플릿을 지원하는 C++ 컴파일러가 없었고, MS의 C++ 툴로 유명한 Visual C++도 버전 6에서는 템플릿을 완벽하게 지원하지 못했으면 Visual Studio .NET 2000 에서부터 제대로 지원을 하게 되었습니다(아직도 템플릿 기능을 100% 완벽하게 지원하지는 못합니다.). 2000년도 이전에 나온 C++ 입문서를 보면 템플릿에 대하여 빠뜨린 것이 꽤 많습니다. 

근래 나오는 입문서에서도 빠져 있기도 하며 또 보통 C++ 입문서에서는 가장 뒷부분에 나오다 보니 공부를 하다가 중간에 포기하는 분들은 클래스라는 것은 알아도 템플릿은 잘 모릅니다. 

개인적으로 C 언어를 생각하면 포인터가 떠오르고, C++ 언어를 생각하면 클래스와 템플릿이 떠올라집니다. 이유는 C 언어나 C++ 언어를 배울 때 정확하게 이해하기 가장 어려웠던 것이고 제가 배웠던 다른 언어들에 비해 크게 달랐던 것이기 때문입니다. 

포인터는 처음 배울 때 문법적인 사용 방법이 잘 이해가 안 돼서 어려웠지만, 클래스나 템플릿은 문법적인 사용 방법이 어려운 것이 아니고 이것들이 프로그램 설계와 관계된 것들이라 사상 부분을 이해하기 어려웠습니다.

객체 지향 프로그래밍(OOP) C++

C++ 언어를 소개할 때 가장 먼저 이야기하는 것이 객체지향이라는 것입니다. 현대 언어들은 거의 다 OOP 베이스의 언어이던가 지원을 하고 있습니다. 

C 언어와 C++ 언어는 이름이 비슷하듯이 비슷한 부분이 많습니다. C 언어로 프로그래밍할 때는 절차 지향적 프로그래밍을 하게 됩니다. C++도 절차 지향 프로그래밍을 할 수 있습니다. 그러나 제대로 된 C++ 프로그래밍을 하려면 객체 지향 프로그래밍을 해야 합니다. 보통 C 언어를 배운 후 바로 이어서 C++를 배울 때는 객체 지향 프로그래밍(Object-Oriented Programming)에 대한 이해가 부족합니다. 그래서 C 언어로 프로그래밍할 때와 같은 절차 지향 프로그래밍을 하여 이른바 'C++를 가장한 C 프로그래밍'을 한다는 소리를 듣기도 합니다. C++ 언어로 객체 지향 프로그래밍을 할 수 있는 것은 C 언어에는 없는 클래스가 있기 때문입니다. 

[질문] C++로 할 수 있는 프로그래밍 스타일은 절차적 프로그래밍, 객체 지향 프로그래밍만 있을까요?
[답] 아니오. Generic Programming 도 가능합니다.

Generic Programming 이라는 것을 들어 보셨나요?

제가 프로그래밍을 배울 때는 일반적으로 C++ 언어를 배우기 전에 C 언어를 공부했습니다. C 언어를 처음 공부했던 시기가 제 기억으로는 1994년쯤입니다. 그 당시의 다른 초보 프로그래머들처럼 포인터의 벽에 부딪혀 좌절하고, 도망(?)가서 3D Studio라는 그래픽 툴을 공부하다가 제가 할 것이 아니라는 생각에 포기하고, 1995년에 다시 C 언어를 공부하였고 이후 바로 C++ 언어를 공부했습니다.

이때도 OOP라는 단어는 무척 자주 들었고 C++로 프로그래밍을 잘한다는 것은 OOP를 잘한다는 것과 같은 뜻이었습니다. 

대학을 다닐 때부터 제 용돈의 많은 부분은 프로그래밍 책을 사는 데 사용되었습니다. 그중에서 저는 C++ 언어 책을 꽤 많이 구매하여 보았습니다(다만, 제대로 이해한 책은 별로 없었습니다. -_-;;; ). 

책에서는 언제나 OOP 단어는 무수히 많이 보았지만, Generic Programming이라는 단어를 그 당시에 본 기억이 없습니다. 제가 Generic Programming이라는 단어를 알게 된 것은 2001년 무렵입니다. C++ 언어를 공부한 지 거의 6년이 되어서야 알게 되었습니다. 

아마 지금 공부하시는 분들도 Generic Programming이라는 단어는 좀 생소할 것입니다. 

Generic Programming은 한국에서는 보통 '일반적 프로그래밍'이라고 이야기 합니다. 저도 처음에는 그렇게 들었습니다. 

그러나 이것은 잘못된 표현이지 않을까 생각합니다. 영어 사전을 보면 Generic 이라는 것은 '총칭(總稱)적인' 이라는 뜻도 있는데 이것이 '일반적'이라는 단어보다 더 확실하며 제가 2004년에 일본에서 구입한 "C++ 설계와 진화(Bjarne Stroustrup 저)"라는 일본 번역서에도 Generic은 총칭으로 표기하고 있습니다. 

그럼 Generic Programming은 무엇일까요? 

네이버 사전에서 Generic 이라는 단어를 검색하면, 

3【문법】 총칭적인
the generic singular 총칭 단수 《보기:The cow is an animal.》 

라는 부분이 있습니다. 보기의 영문을 저의 짧은 영어 실력으로 번역을 하면 '암소는 동물이다' 입니다. 소는 분명히 고양이나 개와는 다른 동물이지만 '동물'이라는 것으로 총칭할 수 있습니다. 

대체 C++언어에서 무엇을 '총칭'화 할까요? 

제가 만드는 프로그램은 Windows 플랫폼에서 실행되는 '온라인 게임 서버' 프로그램입니다. 온라인 게임 서버를 만들 때는 어떤 기능이 있어야 되는가를 정한 후 클래스를 만듭니다. 클래스는 아시는 바와 같이 멤버 변수와 멤버 함수로 이루어져 있습니다. 그리고 멤버 함수도 그 내용은 저의 생각에 의해 변수들이 조작으로 되어 있습니다. 

'암소는 동물이다'라는 식으로 C++ 언어에서 총칭을 하는 것은 변수의 타입(type)을 총칭화 하는 것입니다.
  • 템플릿을 이용하면 총칭화된 타입을 사용하는 클래스와 함수를 만들 수 있습니다.
  • 템플릿을 사용하면 타입에 제약을 받지 않는 로직을 기술 할 수 있습니다.
  • Generic Programming을 하기 위해서는 템플릿이 꼭 필요합니다.
STL이 무엇으로 만들어졌나요? 네 템플릿으로 만들어졌습니다. STL은 Generic Programming으로 만들어진 가장 대표적인 예입니다.

긴 설명은 그만하고 코드를 볼까요? 

제 나름대로 템플릿을 이해하는 데 도움이 되었으면 해서 이런저런 이야기를 했는데 과연 도움이 되었는지 모르겠네요. 아마 설명만 듣고서는 템플릿에 대해 명확하게 이해를 하지 못하리라 생각합니다. 우리 프로그래머들은 정확하게 이해하려면 코드를 봐야겠죠? 템플릿은 크게 함수 템플릿과 클래스 템플릿으로 나눌 수 있습니다. 

다음 시간에는 함수 템플릿에 대해서 이야기하겠습니다.

반응형
반응형
BLOG main image


중요한것은 특수화가 되는 두번째 클래스나 세번째 클래스에서의 class a  옆에 오는 < , > 이 자리에 typename T1, 또는 T2 가 오는 위치이며 특수화시 
template<typename T1> 나 template<typename T2> 또는  template<typename T3> 으로 해도 상관은 없다
(단!  T3으로 했을때 틈수화 템플릿 내부에서도 T3으로 맞춰야한다 )


template<typename T1, typename T2>

class a{

public :

a(){

_t1=T1();

_t2=T2();

std::cout<<_t1<<"\t"<<_t2<<std::endl;

}

T1 _t1;

T2 _t2;

};



template<typename T1>

class a <T1,int>

{

public :

a(){

_t1=T1();

_t2=300;

std::cout<<_t1<<"\t"<<_t2<<std::endl;

}

T1 _t1;

int _t2;

};


template<typename T3>

class a <char,T3>

{

public :

a(){

_t1='t';

_t2=T3();

std::cout<<_t1<<"\t"<<_t2<<std::endl;

}

char _t1;

T3 _t2;

};


int main() 

{

a<int,float> ai1;

a<int,int> ai2;

a<char,float> ai3;

//a<char,int> ai4; //error : T1 과 T2 자리에 대한 특수화가 모두다 있음으로

return 0;

}


반응형
반응형

http://ncanis.tistory.com/334


이외 도움될만한 정보 : http://darkstings.blog.me/30080591302
 

문자열 string

#include "StdAfx.h"
#include "StringTest.h"
#include <algorithm> //transform 사용
#include <functional> //bind1st

using std::string; // 이렇게 지정해준다.


StringTest::StringTest()
{
log("StringTest 생성자 호출");
}

StringTest::~StringTest(void)
{
log("StringTest 소멸자 호출");
}

void StringTest::test()
{
log("============ string class ==========");
// 생성자

std::string s0; // 비어있는 string
std::string s1("012345",3); // 결과: 012
std::string s2("abcdefg",0,1); // 결과:a


log("비어 있는 String=%s, length=%d, isEmpty=%d",s0.c_str(),s0.length(), s0.empty()); // C-String 형으로 리턴, 문자열 + \0
log("문자열로 초기화=%s",s1.data()); // 문자열 내용 반환
log("문자열의 index~index 안 데이터로 생성=%s",s2.c_str());
// 문자열 기본정보

s1.reserve(100); // 메모리 공간 미리 할당
log("가질수있는 문자 최대 갯수=%d",s1.max_size());
log("메모리 재할당 없이 가질수 있는 최대 문자수 =%d",s1.capacity());

// 문자 추가

s1 = "0123456";
s2 = "abcdefg";
s1.append(s2,0,s2.npos); // 결과: 0123456abcdefg , s2의 0~s2.npos 위치의 문자열을 맨뒤에 추가
s1.insert(0,"add"); // 결과: add0123456abcdefg. 문자열을 0번째에 끼어넣기
log("append=%s",s1.c_str()); 


// 문자 복사 copy ( s1 -> s4 로 복사) char* s, size_t n, size_t pos = 0

char s4[100];
int length = s1.copy(s4,s1.length(), 0);
s4[length] = '\0';
log("copy=%s",s4); // 결과: 1234567



// 문자열 원소 접근

s1 ="0123456";
log("0번째 문자 = %c, %c",s1.at(0), s1[1]); // 결과:0,1 =>문자 접근 (오버시 exception 발생)


// 문자 제거

s1 = "0123456";
s1.erase(1); // 결과:0
log("2번째 문자부터 제거=%s",s1.data()); // 문자 접근 (오버시 exception 발생)
s1.empty();


// 문자열 할당 (문자열, 포지션, 갯수)

s1 = "0123456";
s1.assign("abcdef",0,3); // 결과: abc, 0번째 부터 3개문자를 할당
log("assign 문자열 할당 = %s",s1.data()); 


// 문자열 교체

s1 = "abcdef";
s2="0123456";
s1.replace(0,1,"5555"); // 결과: 5555bcdef
s1.swap(s2); // 결과: s1=0123456, s2 = 5555bcdef
log("문자열 교체 s1=%s, s2=%s",s1.data(),s2.data()); 


// substring

s1 = "0123456";
s1 = s1.substr(0,3); // 결과: 012, 0번째부터 3개문자를 짤라 할당한다.
log("substr s1=%s",s1.data()); 


// search

s1 = "01234560123456";
int index = s1.find("2",0);  // 결과 :2, 0번째 위치부터 검색해서 2의 위치를 찾는다.
int index2 = s1.rfind("2");  // 결과 :9, 끝에서 부터 검색해서 2의 위치를 찾는다.
int index3 = s1.find_first_of("13"); // 결과 : 1, 1 or 3이 있는 첫번째 위치를 찾는다.
int index4 = s1.find_last_of("13"); // 결과 :  10, 1 or 3이 있는 마지막 위치를 찾는다.

int index5 = s1.find_first_not_of("0"); // 결과 : 1, 0이 아닌 문자가 나오는 첫번째 위치
int index6 = s1.find_last_not_of("6"); // 결과 : 5, 0이 아닌 문자가 나오는 마지막 위치

// 검색 실패시 std::string::npos 를 리턴한다. std::string::size_type 형이다.
log("search find=%d, rfind=%d, find_first_of=%d, find_last_of=%d, find_first_not_of=%d,find_first_not_of=%d",index,index2,index3, index4,index5, index6); 


// 반복자
ConvertCase();
}

// 반복자 이용
void StringTest::ConvertCase(){
//기본 (영어만 가능)
char str[] = "abcdefg";
strupr(str); // strlwr
log("대소문자 변환=%s",str);

// custom 사용
//std::string msg0("abcdefg");
//std::transform(msg0.begin(), msg0.end(), msg0.begin(), toUpper);

// string 반복자 이용 (영어만 가능)
std::string msg("abcdefg");
std::transform(msg.begin(), msg.end(), msg.begin(), toupper);
log("대소문자 변환=%s",msg.data());

//locale 이용 (모든 나라 가능)
char* org_lc = setlocale(LC_CTYPE, NULL); //현재 locale 저장
setlocale(LC_CTYPE, "spanish");
std::wstring str2(L"pequeño");
std::transform( str2.begin(), str2.end(), str2.begin(), towupper); 
setlocale(LC_CTYPE, org_lc);
log("locale 이용 대소문자 변환=%s",str2.data());

//facet 이용 변환 (모든 나라 가능)
std::locale loc("spanish");
const std::ctype<wchar_t>& ct = std::use_facet<std::ctype<wchar_t> >(loc);
std::wstring str3(L"pequeño");
std::transform(str3.begin(), str3.end(), str3.begin(), 
std::bind1st(std::mem_fun(&std::ctype<wchar_t>::toupper), &ct));
log("facet 이용 대소문자 변환=%s",str3.data());

// 반복자 이용한 출력
msg = "abcdefg";
string::iterator it;
cout<<"반복자 이용한 출력=";
for(it=msg.begin(); it!=msg.end(); ++it) {
cout<<*it;
}
cout<<endl;

// 모든 문자들의 순서 변경
msg = "gfedcba";
std::reverse(msg.begin(), msg.end());
log("순서 바꾸기=%s",msg.data());

// 문자 정렬
std::sort(msg.begin(), msg.end());
log("문자 정렬=%s",msg.data());

// 연속된 문자 제거
msg = "1223334444";
msg.erase(std::unique(msg.begin(), msg.end()), msg.end());
log("연속된 문자 제거=%s",msg.data());


}

//대문자를 소문자로 변환하는 함수 (영어만 가능)
char StringTest::toLower(char elem){
if(elem>='A' && elem<='Z')return elem+('a'-'A');
return elem;
}

//소문자를 대문자로 변환하는 함수 (영어만 가능)
char StringTest::toUpper(char elem){
if(elem>='a' && elem<='z')return elem-('a'-'A');
return elem;
}

//대 소문자 구별없이 일치여부 판단
char StringTest::equlasIgnoreCase(char c1, char c2){
return toUpper(c1)==toUpper(c2);

반응형
반응형
http://ikpil.com/1021

인라인이 무엇인지 알기(exceptional C++ 에 무척 자세히 나온다: 항목 25 : inline 해부 http://www.ikpil.com/821)기 때문에 생략 한다. 

템플릿의 특성상 번역단위마다 코드 인스턴스화가 일어 나지만, 동일한 인스턴스일 때는 단 1개의 정의로 컴파일러는 관리 한다. 그러므로 대부분의 컴파일러에서 "재정의 오류"가 일어나지 않는다. 일어 나는 컴파일러도 있다고 책에 적혀 있지만, 그건 프로그래머 잘못이 아니다. .. 그러므로 프로그래머는 당장 컴파일러를 바꾸어야 한다.(이건 책에 내용이 없다 : ) )

하여튼, 번역단위마다 코드 인스턴스화가 일어 난다고 해서 자동으로 인라인이 된다고 착각 할 수 있다. 이 착각은 인라인과 동일하게 번역 단위마다 코드 붙이기가 일어 난다고 생각 하기 때문이다.

다시 말하지만 이건 착각이다. 그러므로 함수 템플릿이라 하더라도 빠른 호출을 하기 위해선 inline 키워드를 써 주어야 한다.


반응형
반응형
http://saelly.tistory.com/144


- 함수 템플릿과 static지역변수 -


static 지역변수는 템플릿 함수 별로 각각 존재하게 된다.



실행결과로 알 수 있듯이, 컴파일러에 의해서 만들어진 템플릿 함수 별로 static 지역변수가 유지됨을 보이고 있다.






- 클래스 템플릿과 static 멤버변수 -


static 멤버변수는 변수가 선언된 클래스의 객체간 공유가 가능한 변수이다.

템플릿 클래스 별로 static 멤버변수를 유지하게 된다.



32~33행: 10과 15를 더하고 있다, 그래서 25가 출력.


36~37행: 26행에서 정의한 static 특수화로 인하여 mem이 5로 초기화되고 거기에 100이 더해져서 105가 출력된다.


언제 template <typename T> 를 쓰고 언제 template <> 를 쓰는가?


"템플릿 관련 정의에는 template <typename T> 또는 template <> 와 같은 선언을 둬서, 템플릿의 일부 또는 전부를 정의하고 있다는 사실을 컴파일러에게 알려야 한다."


template <typename T>

class Simple

{

public:

T SimpleFunc(T num) { ... }

};

이 경우에는 템플릿의 정의에 T가 등장하므로 template <typename T> 의 선언을 통해서, T가 의미하는 바를 알려야 한다. 




template <>

class Simple<int>

{

public:

int SimpleFunc(int num) { ... }

};

이 정의의 핵심은 <int>이다. 그런데 이 역시 템플릿 관련 정의이기 때문에, 이러한 사실을 알리기 위한 선언이 필요하다. 

하지만 이 정의에서는 T라는 문자가 등장하지 않으니, template <> 을 선언하는 것이다. 


즉, 정의 부분에 T가 존재하면 T에 대한 설명을 위해서 <typename T> 의 형태로 덧붙이면 되고, T가 존재하지 않으면 <>의 형태로 간단하게 선언하면 된다.





- 템플릿 static 멤버변수 초기화의 특수화 -


특수화는 함수 템플릿 또는 클래스 템플릿만을 대상으로 진행할 수 있는 것이 아니다.

클래스 템플릿 정의의 일부인 초기화문을 대상으로도 진행이 가능하다. 방법도 간단하다. 다양한 특수화와 마찬가지로 T를 대신해서 특수화하고자 하는 자료형의 이름을 삽입하면 된다. 


template <>

long SimpleStaticMember<long>::mem = 5;


반응형
반응형


http://blog.naver.com/jinsang83?Redirect=Log&logNo=80093729478



About STL : C++ STL 프로그래밍(6)-해시 맵(Hash Map)

제공 : 한빛 네트워크
저자 : 최흥배

 

About STL을 보시는 분은 대부분 아직 STL을 잘 모르는 분들이라고 생각합니다. 제가 일하고 있는 게임업계는 주력 언어가 C++입니다. 그래서 취업 사이트에 올라온 프로그래머 채용 공고를 보면 필수 조건에 거의 대부분이 C++와 STL 사용 가능이 들어가 있습니다. 게임 업계뿐 아니라 C++을 사용하여 프로그래밍하는 곳이라면 대부분 C++과 STL을 사용하여 프로그램을 만들 수 있는 실력을 필요로 합니다.
C++ 언어를 배우고 사용하는 프로그래머라면 STL을 배우면 좋고, 특히 게임 프로그래머가 되실 분들은 STL을 꼭 사용할 줄 알아야 됩니다.
작년 여름부터 About STL을 쓰기 시작하여 지금은 2009년이 되었습니다. About STL 집필 계획으로는 이제 반 정도 도달한 것 같습니다. 앞으로 남은 반도 STL을 습득하는데 도움이 되도록 저도 최대한 노력할 테니 2009년에는 STL을 꼭 마스터하기를 바랍니다. 

6.1 시퀸스 컨테이너와 연관 컨테이너 

이전 회까지는 STL의 컨테이너에 대해서 설명했었습니다. STL 컨테이너는 크게 시퀸스 컨테이너와 연관 컨테이너로 나눕니다.
시퀸스 컨테이너는 vector, list, deque와 같이 순서 있게 자료를 보관합니다.
연관 컨테이너는 어떠한 Key와 짝을 이루어 자료를 보관합니다. 그래서 자료를 넣고, 빼고, 찾을 때는 Key가 필요합니다. 


[그림 1] 시퀸스 컨테이너와 연관 컨테이너 

시퀸스 컨테이너는 많지 않은 자료를 보관하고 검색 속도가 중요한 경우에 사용하고, 연관 컨테이너는 대량의 자료를 보관하고 검색을 빠르게 하고 싶을 때 사용합니다. 제가 만드는 온라인 게임 서버에서는 보통 접속한 유저들의 정보를 보관할 때 가장 많이 사용합니다. 

6.2 연관 컨테이너로는 무엇이 있을까요? 

연관 컨테이너로 map, set, hash_map, hash_set이 있습니다. 이것들은 Key로 사용하는 값이 중복되지 않은 때 사용합니다. 만약 중복되는 key를 사용할 때는 컨테이너의 앞에 'multi'를 붙인 multi_map, multi_set, hash_multimap, hash_multiset을 사용합니다. Key의 중복 허용 여부만 다를 뿐 사용방법은 같습니다. 

6.2.1 map, set 과 hash_map, hash_set의 차이는? 

가장 쉽게 알 수 있는 큰 차이는 이름 앞에 'hash'라는 단어가 있냐 없냐의 차이겠죠.^^
네, 'hash'라는 단어가 정말 큰 차이입니다. 
map과 set 컨테이너는 자료를 정렬하여 저장합니다. 그래서 반복자로 저장된 데이터를 순회할 때 넣은 순서로 순회하지 않고 정렬된 순서대로 순회합니다. hash_map, hash_set은 정렬 하지 않으며 자료를 저장합니다. 또 hash라는 자료구조를 사용함으로 검색 속도가 map, set에 비해 빠릅니다. 

map, set과 hash_map, hash_set 중 어느 것을 사용할지 생각할 때는
map, set의 사용하는 경우 : 정렬된 상태로 자료 저장을 하고 싶을 때.
hash_map, hash_set : 정렬이 필요 없으며 빠른 검색을 원할 때. 

를 가장 큰 조건으로 보면 좋습니다. 

6.2.2 hash_map, hash_set은 표준은 아닙니다. 

위에 열거한 연관 컨테이너 중 map과 set은 STL 표준 컨테이너지만 hash_map, hash_set은 표준이 아닙니다. 그래서 보통 STL 관련 책을 보시면 hash_map과 hash_set에 대한 설명은 없습니다. hash_map, hash_set을 쓰려면 라이브러리를 설치해야 할까요? 그럴 필요는 없습니다. STL 표준은 아니지만 오래되지 않은 C++ 컴파일러에서는 대부분 지원합니다. 윈도우에서는 Visual Studio.NET의 모든 버전에서 지원합니다.
STL 표준도 아닌 hash_map을 설명하려는 이유는 대부분 C++ 컴파일러에서 지원하고, 새로운 C++ 표준에서는 정식으로 STL에 들어갈 예정이며 현업에서 프로그래밍할 때 아주 유용하게 사용하는 컨테이너이기 때문입니다. 

[참고]
2010년 이내에 새로운 C++ 표준이 만들어질 예정인데 표준이 공표되기 전에 TR1으로 일부 공개하고 있습니다. TR1에서는 hash_map, hash_set과 거의 같은 컨테이너인 unordered_map, unordered_set이 준비되어 있습니다. hash_map, hash_set과 이름만 다를 뿐 컨테이너의 자료구조나 사용방법이 거의 같습니다.
그래서 hash_map, hash_set 사용법을 익히며 자동으로 unordered_map, unordered_set도 익히게 됩니다. 

6.3 hash_map의 자료구조 

hash_map의 자료구조는 '해시 테이블'입니다. 
아래의 [그림 2]에 나와 있듯이 해시 테이블에 자료를 저장할 때는 Key 값을 해시함수에 대입하여 버킷 번호가 나오면 그 버킷의 빈 슬롯에 자료를 저장합니다.
Key 값을 해시 함수에 입력하여 나오는 버킷 번호에 자료를 넣으므로 많은 자료를 저장해도 삽입, 삭제, 검색 속도가 거의 일정합니다. 


[그림 2] 해시 테이블에 자료 넣기 

해시 테이블에 대한 설명은 간단하지 않고 이 글에서는 hash_map 사용법을 간단하게 설명하고 마치려 합니다. 좀 더 자세하게 알고 싶은 분들은 아래의 참고 항목을 꼭 봐 주세요. 

[참고] 해시 테이블 설명
1. http://internet512.chonbuk.ac.kr/datastructure/hash/hash3.htm
2. 좋은 프로그램을 만드는 핵심 원리 25가지(한빛미디어) 

6.4 hash_map을 사용할 때와 사용하지 않을 때 

이전 연재에서 설명한 STL 컨테이너의 장단점은 컨테이너의 자료구조를 보면 알 수 있습니다. hash_map은 해시 테이블을 자료구조로 사용하므로 해시 테이블에 대해 알면 장단점을 파악할 수 있습니다. 해시 테이블은 많은 자료를 저장하고 있어도 검색이 빠릅니다. 그러나 저장한 자료가 적을 때는 메모리 낭비와 검색 시 오버헤드가 생깁니다.
Key 값을 해시 함수에 넣어 알맞은 버킷 번호를 알아 내는 것은 마법 같은 것이 아닙니다. 그러니 hash_map은 해시 테이블을 사용하므로 검색이 빠르다라는 것만 생각하고 무분별하게 hash_map을 사용하면 안됩니다. 컨테이너에 추가나 삭제를 하는 것은 list나 vector, deque가 hash_map보다 빠릅니다. 또 적은 요소를 저장하고 검색할 때는 vector나 list가 훨씬 빠를 수 있습니다. 수천의 자료를 저장하여 검색을 하는 경우에 hash_map을 사용하는 것이 좋습니다. 

hash_map을 사용하는 경우
1. 많은 자료를 저장하고, 검색 속도가 빨라야 한다.
2. 너무 빈번하게 자료를 삽입, 삭제 하지 않는다. 

6.5 Hash_map 사용방법 

STL의 다른 컨테이너와 같이 사용하려면 먼저 헤더 파일과 namespace를 선언해야 합니다. 그러나 여기서 주의할 점은 앞서 이야기 했듯이 hash_map은 표준이 아니므로 표준 STL의 namespace와 다른 이름을 사용하므로 namespace 선언할 때 실수하지 않게 조심하세요.
hash_map 헤더파일을 포함합니다.

#include <hash_map>
hash_map이 속한 namespace는 표준 STL과 다른 'stdext'입니다.
using namespace stdext;
hash_map 선언은 아래와 같습니다.
hash_map< Key 자료 type, Value 자료 type > 변수 이름
위에서는 Value는 저장할 데이터이고, Key는 Value와 가리키는 데이터입니다. 

Key는 int, Value는 float를 사용한다면 아래와 같습니다.
hash_map< int, float > hash1;
다른 컨테이너와 같이 동적 할당을 할 수 있습니다.
hash_map< key 자료 type, Value 자료 type >* 변수 이름 = new hash_map< key 자료 type, Value 자료 type >;
hash_map< int, float >* hash1 = new hash_map< int, float >;
hash_map은 Key와 Value가 짝을 이뤄야 하므로 hash_map을 처음 보는 분들은 이전의 시퀸스 컨테이너와 다르게 좀 복잡하게 보일 것입니다. 그러나 사용이 어려운 것은 아니니 잘 따라와 주세요. 

6.5.1 hash_map의 주요 멤버들 

멤버설명
begin첫 번째 원소의 랜덤 접근 반복자를 반환
clear저장한 모든 원소를 삭제
empty저장한 요소가 없으면 true 반환
end마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase특정 위치의 원소나 지정 범위의 원소들을 삭제
findKey와 연관된 원소의 반복자 반환
insert원소 추가
lower_bound지정한 Key의 요소가 있다면 해당 위치의 반복자를 반환
rbegin역방향으로 첫 번째 원소의 반복자를 반환
rend역방향으로 마지막 원소 다음의 반복자를 반환
size원소의 개수를 반환
upper_bound지정한 Key 요소가 있다면 해당 위치 다음 위치의 반복자 반환


hash_map 컨테이너를 사용할 때는 거의 대부분 추가, 삭제, 검색 이렇게 3가지를 사용합니다. 핵심 기능인 만큼 아래에 좀 더 자세하게 설명하고, 다른 컨테이너는 앞서 설명한 것과 사용방법이 같으므로 예제 코드로 보여 드리겠습니다. 

6.5.2 추가 

insert 

hash_map 에서는 자료를 추가 할 때 insert를 사용합니다.
원형 : 
pair <iterator, bool> insert( const value_type& _Val );
iterator insert( iterator _Where, const value_type& _Val );
template<class InputIterator> void insert( InputIterator _First, InputIterator _Last );
insert를 사용하는 세 가지 방법 중 첫 번째 방식으로 Key 타입은 int, Value 타입은 float를 추가한다면
hash_map<int, float> hashmap1, hashmap2;

// Key는 10, Value는 45.6f를 추가.
hashmap1.insert(hash_map<int, float>::value_type(10, 45.6f));
두 번째 방식으로는 특정 위치에 추가할 수 있습니다.
// 첫 번째 위치에 key 11, Value 50.2f를 추가
hashmap1.insert(hashmap1.begin(), hash_map<int, float>::value_type(11, 50.2f));
세 번째 방식으로는 지정한 반복자 구간에 있는 것들을 추가합니다.
// hashmap1의 모든 요소를 hashmap2에 추가.
hashmap2.insert( hashmap1.begin(), hashmap1.end() );
6.5.3 삭제 

erase
원형 : 
iterator erase( iterator _Where );
iterator erase( iterator _First, iterator _Last );
size_type erase( const key_type& _Key );
첫 번째 방식은 특정 위치에 있는 요소를 삭제합니다.
// 첫 번째 위치의 요소 삭제.
hashmap1.erase( hashmap1.begin() );
두 번째 방식은 지정한 구역에 있는 요소들을 삭제합니다.
// hashmap1의 처음과 마지막에 있는 모든 요소 삭제
hashmap1.erase( hashmap1.begin(), hashmap1.end() );
세 번째 방식은 지정한 키와 같은 요소를 삭제합니다.
// Key가 11인 요소 삭제.
hashmap1.erase( 11 );
첫 번째와 두 번째 방식의 반환 값으로는 삭제된 요소의 다음의 것을 가리키는 반복자이며 세 번째 방식은 삭제된 개수를 반환합니다. 

6.5.4 검색 

hahs_map에서 검색은 Key를 사용하여 같은 Key를 가지고 있는 요소를 찾습니다.
Key와 같은 요소를 찾으면 그 요소의 반복자를 반환하고, 찾지 못한 경우에는 end()를 가리키는 반복자를 반환합니다.
원형 : 
iterator find( const Key& _Key );
const_iterator find( const Key& _Key ) const;
방식은 두 가지지만 사용법은 같습니다. 차이는 반환된 반복자가 const냐 아니냐의 차이입니다. 참고로 첫 번째 방식은 const가 아니므로 찾은 요소의 Value를 변경할 수 있습니다(참고로 Key는 변경 불가입니다). 두 번째 방식은 Value도 변경할 수 없습니다.
// Key가 10인 요소 찾기.
hash_map<int, float>::Iterator FindIter = hashmap1.find( 10 );

// 찾았다면 Value를 290.44로 변경
If( FindIter != hashmap1.end() )
{
   FindIter->second = 290.44f;
}
begin, clear, count, empty, end, rbegin, rend, size는 앞서 말 했듯이 다른 컨테이너와 사용방법이 비슷하므로 아래 예제 코드를 통해서 사용법을 보여 드리겠습니다. 

[리스트 1] hash_map을 사용한 유저 관리
#include <iostream>
#include <hash_map>
using namespace std;
using namespace stdext;

// 게임 캐릭터
struct GameCharacter
{
  // 아래의 인자를 가지는 생성자를 정의한 경우는
  // 꼭 기본 생성자를 정의해야 컨테이너에서 사용할 수 있다.
  GameCharacter() { }

  GameCharacter( int CharCd, int Level, int Money )
  {
    _CharCd = CharCd;
    _Level = Level;
    _Money = Money;
  }
  int _CharCd;    //  캐릭터 코드
  int _Level;    // 레벨
  int _Money;    // 돈
};

void main()
{
  hash_map<int, GameCharacter> Characters;

  GameCharacter Character1(12, 7, 1000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(12, Character1));

  GameCharacter Character2(15, 20, 111000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(15, Character2));

  GameCharacter Character3(200, 34, 3345000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(200, Character3));
  
  // iterator와 begin, end 사용
  // 저장한 요소를 정방향으로 순회
  hash_map<int, GameCharacter>::iterator Iter1;
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl; 

  // rbegin, rend 사용
  // 저장한 요소의 역방향으로순회
  hash_map<int, GameCharacter>::reverse_iterator RIter;
  for( RIter = Characters.rbegin(); RIter != Characters.rend(); ++RIter )
  {
    cout << "캐릭터 코드 : " << RIter->second._CharCd << " |   레벨 : " << RIter->second._Level 
      << "|  가진 돈 : " <<  RIter->second._Money << endl; 
  }
  cout << endl << endl;
 
  // Characters에 저장한 요소 수
  int CharacterCount = Characters.size();

  // 검색. 
  // 캐릭터 코드 15인 캐릭터를 찾는다.
  hash_map<int, GameCharacter>::iterator FindIter = Characters.find(15);
  // 찾지 못했다면 FindIter은 end 위치의 반복자가 반환된다.
  if( Characters.end() == FindIter )
  {
    cout << "캐릭터 코드가 20인 캐릭터가 없습니다" << endl;
  }
  else
  {
    cout << "캐릭터 코드가 15인 캐릭터를 찾았습니다." << endl;
    cout << "캐릭터 코드 : " << FindIter->second._CharCd << " |   레벨 : " << FindIter->second._Level 
      << "|  가진 돈 : " <<  FindIter->second._Money << endl;
  }
  cout << endl;

  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl << endl;
  
  // 삭제
  // 캐릭터 코드가 15인 캐릭터를 삭제한다.
  Characters.erase( 15 );
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl << endl;

  // 모든 캐릭터를 삭제한다.
  Characters.erase( Characters.begin(), Characters.end() );

  // Characters 공백 조사
  if( Characters.empty() )
  {
    cout << "Characters는 비어 있습니다." << endl;
  }
}
결과 

 

6.5.5 lower_bound와 upper_bound 

hash_map에 저장한 요소 중에서 Key 값으로 해당 요소의 시작 위치를 얻을 때 사용하는 멤버들입니다. Key 값의 비교는 크기가 아닌 저장 되어 있는 요소의 순서입니다. 23, 4, 5, 18, 14, 30 이라는 순서로 Key 값을 가진 요소가 저장되어 있으며 Key 값 18과 같거나 큰 것을 찾으면 18, 14, 30이 됩니다. 

lower_bound 

Key가 있다면 해당 위치의 반복자를 반환합니다.
원형 : 
iterator lower_bound( const Key& _Key );
const_iterator lower_bound( const Key& _Key ) const;
upper_bound 

Key가 있다면 그 요소 다음 위치의 반복자를 반환합니다.
원형 : 
iterator lower_bound( const Key& _Key );
const_iterator lower_bound( const Key& _Key ) const;
lower_bound와 upper_bound는 hahs_map에 저장된 요소를 일부분씩 나누어 처리를 할 때 유용합니다. 예를 들면 hash_map에 3,000개의 게임 캐릭터 정보를 저장되어 있으며 이것을 100개씩 나누어서 처리하고 싶을 때 사용하면 좋습니다. 

[리스트 2] lower_bound와 upper_bound 사용 예
#include <iostream>
#include <hash_map>
using namespace std;
using namespace stdext;

// 게임 캐릭터
struct GameCharacter
{
  GameCharacter() { }

  GameCharacter( int CharCd, int Level, int Money )
  {
    _CharCd = CharCd;
    _Level = Level;
    _Money = Money;
  }
  int _CharCd;    //  캐릭터코드
  int _Level;    // 레벨
  int _Money;    // 돈
};

void main()
{
  hash_map<int, GameCharacter> Characters;

  GameCharacter Character1(12, 7, 1000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(12, Character1));

  GameCharacter Character2(15, 20, 111000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(15, Character2));

  GameCharacter Character3(7, 34, 3345000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(7, Character3));

  GameCharacter Character4(14, 12, 112200 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(14, Character4));

  GameCharacter Character5(25, 3, 5000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(25, Character5));

  hash_map<int, GameCharacter>::iterator Iter1;
  cout << "저장한 캐릭터 리스트" << endl;
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl;

  cout << "lower_bound(14)" <<endl;
  hash_map<int, GameCharacter>::iterator Iter = Characters.lower_bound(14);
  while( Iter != Characters.end() )
  {
    cout << "캐릭터 코드 : " << Iter->second._CharCd << " |   레벨 : " << Iter->second._Level 
      << "|  가진 돈 : " <<  Iter->second._Money << endl;

    ++Iter;
  }
  cout << endl;

  cout << "upper_bound(7)" <<endl;
  Iter = Characters.upper_bound(7);
  while( Iter != Characters.end() )
  {
    cout << "캐릭터 코드 : " << Iter->second._CharCd << " |   레벨 : " << Iter->second._Level 
      << "|  가진 돈 : " <<  Iter->second._Money << endl;

    ++Iter;
  }
}

결과 

 

이것으로 hash_map의 주요 사용법에 대한 설명이 끝났습니다.
위에 설명한 것들만 알고 있으면 hash_map을 사용하는데 문제가 없을 것입니다.
위에 설명한 것들 이외의 hash_map의 멤버들에 대해서 알고 싶으며 마이크로소프트의 MSDN 사이트에 있는 것을 참고하세요
http://msdn.microsoft.com/en-us/library/h80zf4bx(VS.80).aspx 

[참고]
Visual C++의 hash_map의 성능에 대해서 Visual C++에 있는 hash_map은 다른 컴파일에서 구현한 것보다 꽤 느리다라는 말이 있습니다.
(관련 글은 여기서 참고하세요. http://minjang.egloos.com/1983788 http://junyoung.tistory.com/1

얼마나 느린지 테스트했습니다.
http://blog.naver.com/jacking75/140062720030
제가 조사한 것은 Windows 플랫폼에서 VC++에서 제공한 라이브러리로 테스트한 것입니다.
결과를 보면 hash_map이 map보다 빠르지도 않고 특히 hash_map과 같은 자료구조를 사용하는 컨테이너로 마이크로소프트사에서 만든 CAtlMap에 비해 속도가 아주 느립니다.
성능이 중요한 곳에 hash_map을 사용한다면 VC++에 있는 것을 사용하지 말고 자체적으로 잘 만들어진 hash 함수를 사용하거나 C++ 오픈 소스 라이브러리인 boost에 있는 unordered_map을 사용하는 것이 좋을 것 같습니다. Windows 플랫폼에서만 사용한다면 CAtlMap을 사용하는 것도 좋습니다.

 

출처 : http://network.hanb.co.kr/view.php?bi_id=1617

반응형
반응형



http://ikpil.com/551


내가 STL에 조예가 깊어서 글을 남기는 것이 아니라, Effecitve STL 을 공부하는 사람들이 이 글을 보고, 도움이 되었으면 하는 생각과, 혹시 내가 틀린것이 있다면 지적해 주시지 않을까 란 생각으로 글을 올리는것임을 미리 밝힙니다.  - 최익필




우리말이 자주 어렵다고 생각한다. 글을 쓸 때, 문맥상 이상하게 이어지는 경우가 많기 때문이다. 그래서 "A는 B이다" 식으로 이야기를 하려고 한다.

이번 주제는 reverse_iterator.base() 가 어떻게 동작하는지에 대한 이야기이다. 왜냐하면 insert() 나 erase()로 reverse_iterator 를 사용할 수 없어, iterator 로 변환하는데 base() 함수를 사용 하기 때문이다.


base()로 가리킨 iterator 는 무엇을 가리키고 있는가? 
실험을 통해 vector에 5개의 데이터 1,2,3,4,5 를 넣어보고, reverse_iterator 가 무엇을 가리키고 iterator 변환시 무엇을 가리키는지 알아 보겠다.



// ikpil.com or ikpil.tistory.com
#include <vector>
#include <algorithm>
#include <iostream>
 
typedef std::vector<int> Container;
 
int main( void )
{
    Container Test( 5 );
 
    for( int i = 0; i < 5; ++i)
        Test[i] = i+1;
 
    typedef Container::reverse_iterator Reverse_It;
    typedef Container::iterator  Nomal_It;
 
    // rit 용 3을 찾음
    Reverse_It rit = std::find( Test.rbegin(), Test.rend(), 3 );
    std::cout << "rit = " << *rit << std::endl;
 
    // rit 을 it 으로 변환
    Nomal_It it = rit.base();
    std::cout << "rit.base() 해서 it을 초기화 " << std::endl;
    std::cout << "it = " << *it << std::endl;
    std::cout << " 그렇다 rit.base()의 it과 rit과 가르킨 대상이 다르다!" << std::endl;
 
    // 다시 한번 테스트
 
    // rit 용 2를 찾음
    rit = std::find( Test.rbegin(), Test.rend(), 2 );
    std::cout << "rit = " << *rit << std::endl;
 
    // rit 을 it 으로 변환
    it = rit.base();
    std::cout << "rit.base() 해서 it을 초기화 " << std::endl;
    std::cout << "it = " << *it << std::endl;
    std::cout << "rit.base()는 가리킨 대상의 다음것을 it에 지정해 준다,!" << std::endl;
}



?



자.. 규칙을 찾앗다. base()로 변환된 iterator는 순향반복자 기준으로 다음것을 가리킨다. 만약 insert 나 erase 하게 될 경우, 이 부분을 알고서 사용 해야 할 것이다.

... 어떻게 사용 하냐고? .. 음.. 아래 코드를 보면 된다.


// ikpil.com or ikpil.tistory.com
#include <vector>
#include <algorithm>
#include <iostream>
 
typedef std::vector<int> Container;
typedef Container::reverse_iterator Reverse_It;
typedef Container::iterator  Nomal_It;
 
int main( void )
{
    Container Test( 5 );
 
    for( int i = 0; i < 5; ++i)
        Test[i] = i+1;
 
    // rit 용 3을 찾음
    Reverse_It rit = std::find( Test.rbegin(), Test.rend(), 3 );
     
     Test.erase((++rit).base()); // or Test.erase(--rit.base());
     // 책에선 --rit.base() 는 컴파일 되지 않는 다고 하지만
     // MSVC2005에서 잘 된다.   

책에선 안된다는 --rit.base() 해 보았지만, MSVC2005 에선 잘된다. 그래도 컴파일 특성이 다를 수 있으니 (++rit).base() 로 사용하게 좋을 듯 싶다.^^(내부 iterator 가 const_iterator 가 아닌 iterator로 반환하는것을 확인)

사용자 삽입 이미지

그림으로 표현된 reverse_iterator 와 iterator의 차이

개인적인 생각
base() 호출시 바로 it에 대응하는 것을 벹어 낼수도 있는데 왜 항상 다음것을 벹어 내는가? 그 이유는reverse_iterator의 rend() 과 iterator의 begin() 을 매칭 시키기 위해서다. 이렇게 하는 것은 end() 도 자연스럽게 맞쳐지며, 구현자나 사용자나 보다 쉽게 이용할수 있기 때문이라고 개인적으로 생각 한다.(뭐.. base()가 대응되는게 한칸 뒤라는것을 기억해야 하겠지만.^^)


이것만은 잊지 말자
1. base() 가 뱉어내는 iterator는 reverse_iterator 의 전 원소이다.(이해가 어렵다면,, 위의 코드를 보는게 더 도움이 될듯)


관련링크
http://alones.kr/blog/841 <-- 이것도 같은 설명~
http://ilu8318.egloos.com/797949 <-- 이것도 같은 설명



반응형
반응형

http://blog.naver.com/ships95/120102532486




아무리 훌륭한 디버거가 있더라도 모든 경우에 디버거를 사용할 수 있는 것은 아닙니다. 특히 윈도우 서비스를 만들 때에는 디버거를 사용하기 꽤 까다롭죠? 한 가지 방법이 있다면 서비스 프로세스가 생성된 다음에 디버거에 연결해서 디버깅할 수 있는데 만일 프로세스 생성과정에서 일어나는 것에 대한 디버깅은 역시 어렵게 됩니다. 이럴 때 보통 이벤트 뷰어를 사용하게 되는데 개발 과정에서 이벤트를 뷰어를 사용하는 것도 여간 번거로운 일이 아닙니다. 보통 이런 상황에서는 원시적이긴 하지만 로그 파일을 사용하는 것이 일반적이 방법입니다.
최근 다국어 환경을 염두해 두고 개발을 하고 있는데 로그 파일을 시스템에 독립적인 유니코드를 사용하면 다국화 환경을 지원하는 데 좀 더 편리하겠다는 생각에 프로그램 내부 문자열을 모두 wchar_t를 사용하여 유니코드로 변경하고 STL의 wfstream(또는 wofstream)을 사용하여 로그 파일을 출력하려고 했습니다. 하지만 STL의 wfstream이 기본적으로 wchar_t형의 문자열을 유니코드로 출력을 하지 않는다는 것이 이제야 알게 되었습니다. 표준화는 접어 두고라도 출력 연산자(<<)의 유혹에 wfstream으로 유니코드 출력하는 방법을 알아 보게 되었습니다.

STL의 입출력 스트림(Input/Output Stream)을 통한 입출력 방식은 시스템에 설정된 언어 문화적 환경에 의해 방식이 바뀌게 되어 있습니다. 예를 들어 한국어를 사용하는 환경에서 날짜는 주로 “2009년 12월 12일 토요일”로 표기하지만, 영어를 사용하는 환경에서는 주로 “Tuesday, December 15, 2009″라고 표기합니다. 이런 언어 문화적 환경을 결정하는 것을 로켈(locale)이라고 합니다.
이러한 로켈은 숫자(Numeric), 시간/날짜(Time/Date), 화폐(Monetary), 문자(Character Type), 문자열 비교(Collation), 메시지(Message) 등 여섯 개 범주로 언어 환경을 지정하게 됩니다. 이 여섯 개의 범주는 다시 패싯(Facet)이라는 것을 통해 실제 구현됩니다. 이제 프로그래머는 응용 프로그램에서 사용할 여섯 개의 범주의 패싯을 로켈에 설정하고 이 로켈을 입출력 스트림에 적용하면 프로그램 내부와 외부 사이의 입출력을 제어할 수 있게 되는 것입니다. 입출력 스트림, 로켈, 패싯의 관계를 그림으로 보면 다음과 같습니다.

패싯 작성, 로케일의 생성 및 입출력 스트림에 적용하는 방법은 어렵지 않으니 STL 문서를 참고하시기 바랍니다. (그렇다고 그다지 쉬운 것은 아닙니다. ^^)
제가 관심있는 부분은 유니코드를 출력하는 부분인데 이 부분은 문자 범주에 속합니다. 문자 관련 언어 환경이란 대소문자, 숫자, 부호 등을 구분하는 방식과 문자의 이진수 표현 방식 즉 문자 부호화 또는 인코딩(Character Encoding)을 지정하는 것입니다. 유니코드라는 것이 문자열을 이진수로 표현하는 인코딩 방법이기 때문에 문자 인코딩 관련 패싯을 잘 만들어서 로켈에 적용하고 이를 입출력 스트림에 적용하면 되는 것입니다.
다행인 것은 현재 개발 중에 있는 대부분(사실 모든) 프로젝트는 프로그램 내부에서는 유니코드를 사용하고 있기 때문에 내부 인코딩 상태를 그대로 내보내는 패싯을 만들기만 하면 된다는 것입니다. 즉 아무런 변환 없이 그대로 내보내면 된다는 것입니다. 앞서 이야기한 대로 STL에 기본 구현된 codecvt 패싯은 wchar_t를 다중 바이트 문자열로 변환을 하도록 되어 있어 유니코드 문자열을 그대로 출력할 수 없는 것입니다.
인코딩 패싯을 처음부터 만들려면 인코딩 구현뿐만 아니라 패싯이 가져야 할 다양한 기능을 구현해야 하는데 쉬운 일이 아닙니다. 따라서, STL이 기본적으로 구현해 놓은 codecvt를 적절히 확장해서 쉽게 구현할 수 있습니다.
아래 예는 비주얼 C++과 함께 제공되는 STL codecvt를 사용하였지만 다른 STL 구현도 크게 차이가 없을 것으로 보입니다.

 
1class Unicodecvt : public std::codecvt<wchar_tcharmbstate_t>
2{
3protected:
4    virtual bool do_always_noconv() const
5    {
6        return true;
7    }
8};
 

위의 Unicodecvt는 사실은 유니코드로 변환하는 패싯이 아니라 wchar_t를 아무 변환없이 출력하는 패싯이기 때문에 진정한 의미의 유니코드 패싯이라고 할 수 없지만 앞서 언급한 것처럼 내부 코드를 유니코드를 사용하고 있다면 가장 간단히 구현할 수 있는 패싯이라고 생각됩니다. 이 패싯을 이용해서 유니코드로 출력하는 예는 아래와 같습니다.

 
01#include <fstream>
02void main(void)
03{
04    std::wofstream log(L"test.log", std::ios_base::binary);
05    log.imbue(std::locale(std::locale(""), new Unicodecvt));
06    wchar_t* msg = L"오류가 발생했습니다.";
07    int error_no = 1;
08    log << wchar_t(0xFEFF);
09    log << msg << L" 오류번호: " << error_no << std::endl;
10}
 

위의 예에서 4번째 줄에 std::ios_base::binary를 사용한 이유는 앞에서 정의한 Unicodecvt를 사용하게 되면 fstream의 << 연산자가wchar_t형의 문자를 출력할 때 내부적으로 fputwc라는 C 표준 함수를 사용하는데 이 함수가 파일을 일반 모드로 파일을 열게 되면 오류를 발생시키기 때문입니다. 다른 STL 구현에서는 어떻게 되는지 아직 확인하지 못했습니다. 또한 8번째 줄에 0xFEFF를 파일 처음에 출력(파일에는FFFE 순으로 저장됨)하는데 이것을 바이트 순서 표시(Byte Order Mark, BOM)라고 합니다. 이것은 파일이 16바이트 유니코드 인코딩을 사용해 만들어졌다는 것을 의미하는 것으로 메모장과 같은 대부분의 텍스트 파일 편집기가 이것을 해석하여 파일의 내용을 옳바르게 보여주게 됩니다.

비주얼 C++의 STL을 사용해서 유니코드 텍스트 파일 출력에 대해서 간단히 알아봤는데 C++의 표준 입출력 스트림의 국제화에 대한 좀 더 자세한 내용은 니콜라이 조슈티스(Nicolai M. Josuttis)의 The C++ Standard Library의 14장 국제화(Chapter 14 Internationalization)을 참고하시기 바랍니다.

반응형
반응형

-연속메모리 컨테이너 : vector, string( or rope), deque = 배열로 이루어졌으며 재하당 이 일어날 수 있다

-노드기반 컨테이너 :  set, multiset,map,multimap, 요소 삽입을 임의의 위치에서 삽입 불가능( 순서가 있으므로 )

                               (balanced tree 로 구성됨)

 

-해쉬컨테이너와 slist(단일연결리스트) 의 경우 양방향 반복자로 접근될 수 없다

 

-배열 구조와 호환되어야 한다면 vector밖에 쓸 수 있는게 없다

 

-탐색속도가 관심사라면 => 1.해쉬컨테이너, 2.정렬된 vector, 3. 표준 연관(set,map, 외..) 컨테이너 중 하나 의 순으로 접근한다

 

- 참조카운팅이 고려할때는 string 을 쓰지 말 것(string 내부에서 참조카운팅을 사용 할 수 있다=> 동일한 문자열에 대한 참조카운팅 )

   rope 또한 마찬가지 => 참조 카운팅 기반으로 구현되어 있다

   : 대신할 수 있는 것 vector<char>

 

-트렌젝션이 가능하거나 포인터,반복자, 참조가의 무효화가 최소화 되어야 하는가? => 노드기반 컨테이너( list, set, map )

 

 

 

삽입 


set,map 의 경우 push_back, push_front 를 지원하지 않는다 => insert 로 써야 함, 순서유지

list 의 경우에는 지원

 

 

예약

vector 에서는 reserve,capacity 가 있지만 list, deque 에서는이 멤버함수가 없다(내부 구조 때문)

 

반응형
반응형

이펙티브 STL 카페


http://cafe.naver.com/ArticleList.nhn?search.clubid=10513818&search.menuid=59&search.boardtype=Q

이펙티브 STL 게시글


http://cafe.naver.com/cppstl.cafe?iframe_url=/ArticleList.nhn%3Fsearch.clubid=10513818%26search.menuid=59%26search.boardtype=Q







http://cafe.naver.com/cppstl

반응형
반응형

내가 STL에 조예가 깊어서 글을 남기는 것이 아니라, Effecitve STL 을 공부하는 사람들이 이 글을 보고, 도움이 되었으면 하는 생각과, 혹시 내가 틀린것이 있다면 지적해 주시지 않을까 란 생각으로 글을 올리는것임을 미리 밝힙니다. - 최익필





이번 항목은 두 알고리즘의 사용 방법을 알아보자는 취지로 이야기 된 것 같다. 그러므로 나는 두 알고리즘의 사용 법을 기준으로
 설명을 하도록 한다

두 알고리즘 전부 algorithm 안에 들어 있다.

각각 어떻게 사용 하는지, 그 소스코드를 보자.

#include <iostream>
#include <algorithm>
#include <vector>
 
int main( void )
{
    int a[10];
    std::vector<int> v(10);
    for( int i = 0; i < 10; i++ )
    {
        a[ i ] = i;
        v[ i ] = i;
    }
    // 일부러 틀리게 셋팅
    v[ 3 ] = 10;
 
    // 비교 후 틀리면
    // 틀린 위치의 a 배열 값과 v 컨테이너 이터레이터를 뱉어냄
    std::pair<int*, std::vector<int>::iterator > p =
        std::mismatch( &a[0], &a[9], v.begin() );
 
    // 만약 틀린게 없다면, 둘다 끝의 값을 p 에 저장함
    std::cout << *p.first << std::endl;
    std::cout << *p.second << std::endl;
     
    return 0;
}


한가지 주의 해야 할 점은 첫번째 범위 데이터는 두번째 범위데이터 보다 짦은 범위의 인자를 넣어야 한다는 것이다. 다른것은 그냥 컴파일 해보면 알수 있을 것이다.

#include <iostream>
#include <algorithm>
#include <vector>
 
int main( void )
{
    int a[10];
    std::vector<int> v(10);
    for( int i = 0; i < 10; i++ )
    {
        a[ i ] = i;
        v[ i ] = i;
    }
    // 일부러 틀리게 셋팅
    v[ 3 ] = 10;
 
    // 기본적으로 less 비교를 한다.
    // 리턴값 : 첫번째 범위 구역의 값들 중 두번째 범위구역 값들보다
    // 작거나 같으면 TRUE 아니라면 FALSE
    if( std::lexicographical_compare(&a[0], &a[9], v.begin(), v.end()) )
    {
        std::cout << "성공" << std::endl;
    }
    else
    {
        std::cout << "실패" << std::endl;
    }
 
    return 0;
}

주의 해야 할 구역은 주석을 달아 두었다.


관련링크
http://ilu8318.egloos.com/833073
http://turboc.borlandforum.com/impboard/impboard.dll?action=read&db=cpp_tip&no=19
http://www.winapi.co.kr/clec/cpp4/42-1-4.htm - mismatch 설명

반응형
반응형

상등 ==  set.find

동등 < 로 판정 find(

 

set 멤버 비교, 동등비교

ex) set의 멤버함수인 find,  그냥 find 알고리즘

 

set멤버함수 find 함수는 동등 비교

그냥 알고리즘 비교 -> 상등 비교

 


반응형
반응형

http://ohyecloudy.com/pnotes/archives/243


[STL] 문자열과 integer,float 사이의 변환 (atoi, stringstream)

 

#include <cstdlib>
#include <cstdio>
#include <cerrno>
 
int main()
{
    char    *str = NULL;
    int     value = 0;
 
    str = "-123124";
    value = atoi( str );
    printf( "Function: atoi( \"%s\" ) = %d\n", str, value );
 
    str = "3336402735171707160320";
    value = atoi( str );
    printf( "Function: atoi( \"%s\" ) = %d\n", str, value );
    if (errno == ERANGE)
    {
        printf("Overflow condition occurred.\n");
    }
 
    return 0;
}

 

문자열에서 integer, float로 캐스팅이 필요할 때, 주로 atoi, atof, strtol 등 CRT 함수를 사용한다. underflow, overflow가 발생하면 errno이 ERANGE로 세팅되고 최대값 혹은 최소값을 반환한다. 위 예제 코드는 MSDN 에서 가져왔다.

 

 

int stoi(const string& from)
{
    std::stringstream oss(from);
 
    int to;
    oss >> to;
    assert(!oss.fail() && !oss.bad() && oss.eof());
    if( oss.fail() || oss.bad() || !oss.eof() )
    {
        // 예외 처리
    }
 
    return to;
}

atoi를 STL에 있는 stringstream class를 사용해서 구현할 수 있다. “123d”와 같은 문자열을 문자열 스트림에 넣고 int로 출력하면 d 이전까지만 출력된다. 이 경우 스트링 문자열에 문자가 남아있게 된다. 이렇게 입력 문자열이 숫자로만 구성됐는지를 검사하려면 eof 플래그를 검사하면 된다. “d123″ 같이 처음부터 출력하지 못하는 경우나 overflow나 underflow 경우에는 fail이나 bad 플래그가 세팅된다. 그래서 이 세 개의 플래그를 검사해서 예외 처리를 하면 된다. underflow, overflow 같은 경우는 atoi를 사용한 예제처럼 errno 값을 검사해서 확인할 수도 있다.

 

#include <sstream>
#include <iostream>
#include <string>
#include <assert.h> 


template<typename TO, typename FROM>
TO NumberStringCast( const FROM& from )
{
    stringstream ss;
    ss << from;
 
    TO result;
    ss >> result;
 
    assert(!ss.fail() && !ss.bad() && ss.eof());
    if( ss.fail() || ss.bad() || !ss.eof() )
    {
        // 예외 처리
    }
 
    return result;
}
 
int main()
{
    cout << NumberStringCast<int>("123") << endl;
    cout << NumberStringCast<string>(12.5f) << endl;
    cout << NumberStringCast<float>("123.3e10") << endl;
 
    return 0;
}

템플릿 클래스로 만들면 추가 코드 작성 없이 변환을 할 수 있다.

실행 속도는 당연히 CRT 함수인 atoi 시리즈가 당연히 빠르다. 속도에 민감하지 않는 부분이면 stringstream을 사용해서 편하게 캐스팅하는게 나을 것 같다. stringstream는 iostream 클래스를 상속받기 때문에 iostream처럼 다룰 수 있다는게 최고 장점이다. 이런게 있다는 것만 알고 사용하지 않았는데, 사용해보니 상당히 편하다.

반응형
반응형

tolower ,toupper a~z, A~Z 의 범위를 변환 시킨다






http://neodreamer.tistory.com/267



std::string 의 문자열을 대소문자 변환이 필요해 찾아보았는데 쉽게 처리할 수 있는 방법을 찾았다.

std::transform 함와 cctype 헤더의 tolower() 와 toupper() 함수를 이용하면 쉽게 해결이 되었다.

transform 함수는 이외에도 응용할 부분이 많아보인다. container 의 각 원소에 특정을 변형을 주어
바꾸거나 다는 container에 넣을 수도 있다. transform 함수에 대해서는 좀더 공부를 해서 나중에 포스팅을 한번 해야겠다.



#include <cctype> // for toupper & tolower
#include <string>
#include <algorithm>
using namespace std;


	// 대문자로 바꾸기
	string s1 = "sample string";
	transform( s1.begin(), s1.end(), s1.begin(), toupper );
	// 결과 : s1	"SAMPLE STRING"

	// 소문자로 바꾸기
	string s2 = "HELLO";
	transform( s2.begin(), s2.end(), s2.begin(), tolower );
	// 결과 : s2	"hello"


	// 첫 문자만 대문자로 바꾸기
	string s3 = "title";
	transform( s3.begin(), s3.begin() + 1, s3.begin(), toupper );
	// 결과 : s3	"Title"

반응형
반응형



bitset 으로 int 를 이진수로 표현하기



#include <bitset>
#include <iostream>
using namespace std;

 

 

 

int dd=3;
cout << "dd as binary short : " << bitset<numeric_limits<unsigned short>::digits>( dd ) << endl;


or

 

cout << "3 as binary short : " << bitset<numeric_limits<unsigned short>::digits>( 3 ) << endl;






http://www.borlandforum.com/impboard/impboard.dll?action=read&db=cpp_qna&no=563



10진수를 진법을 변환한다고 한다면 문자열로 변환한다는 거겠죠? 

방법 1) 
stdlib.h 에 있는 
itoa(), ltoa(), ultoa() 함수를 쓰면 (Windows에만 있습니다. ANSI C 호환 안됩니다.) 
int, long, unsigned long 형을 2~36진법(숫자 10 + 영문자 26)까지 선택하여 
char* 형으로 변환할 수 있습니다. 

itoa의 원형은 다음과 같습니다. 
char *itoa(int value, char *string, int radix); 

예제는 다음과 같습니다. 

int main() 

   int number = 123; 
   char string[25]; 

   itoa(number, string, 2); 
   printf("integer = %d string = %s\n", number, string); 
   return 0; 


방법2) 
2진법만으로의 변환이라면, 
ANSI C++ 라이브러리의 bitset.h에 있는 bitset 클래스를 사용하면 됩니다. 
10진수 -> 2진수, 2진수 -> 10진수로의 변환도 쉽게 할 수 있습니다. 
물론 속도는 itoa()보다 빠릅니다. 

예제는 다음과 같습니다. 

int main() { 
  const bitset<12> mask(2730ul); 
  cout << "mask =      " << mask << endl; 

  bitset<12> x; 

  cout << "Enter a 12-bit bitset in binary: " << flush; 
  if (cin >> x) { 
    cout << "x =        " << x << endl; 
    cout << "As ulong:  " << x.to_ulong() << endl; 
    cout << "And with mask: " << (x & mask) << endl; 
    cout << "Or with mask:  " << (x | mask) << endl; 
  } 

  return 0; 


출력은 다음과 같습니다. 

mask =      101010101010 
Enter a 12-bit bitset in binary: 111000111000 
x =        111000111000 
As ulong:  3640 
And with mask: 101000101000 
Or with mask:  111010111010 

참고) 정수를 16진수로 변환할 때는 VCL의 IntToHex()도 많이 사용합니다. 
단 이 경우는 AnsiString으로 변환합니다.



반응형

+ Recent posts